Merge history of CTS
BUG: 167962976
Test: TH
Merged-In: I30d52c4df571c894b7797300e8f56ddd4b2cc2dd
Change-Id: Id343f5a31604abfe70140343bffab20503cd2705
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
new file mode 100644
index 0000000..47b114b
--- /dev/null
+++ b/tests/cts/hostside/Android.bp
@@ -0,0 +1,29 @@
+// Copyright (C) 2014 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.
+
+java_test_host {
+ name: "CtsHostsideNetworkTests",
+ defaults: ["cts_defaults"],
+ // Only compile source java files in this apk.
+ srcs: ["src/**/*.java"],
+ libs: [
+ "cts-tradefed",
+ "tradefed",
+ ],
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+}
diff --git a/tests/cts/hostside/AndroidTest.xml b/tests/cts/hostside/AndroidTest.xml
new file mode 100644
index 0000000..b7fefaf
--- /dev/null
+++ b/tests/cts/hostside/AndroidTest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<configuration description="Config for CTS net host test cases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="networking" />
+ <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+ <target_preparer class="com.android.compatibility.common.tradefed.targetprep.LocationCheck" />
+ <target_preparer class="com.android.cts.net.NetworkPolicyTestsPreparer" />
+
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="teardown-command" value="cmd power set-mode 0" />
+ <option name="teardown-command" value="cmd battery reset" />
+ <option name="teardown-command" value="cmd netpolicy stop-watching" />
+ </target_preparer>
+
+ <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+ <option name="jar" value="CtsHostsideNetworkTests.jar" />
+ <option name="runtime-hint" value="3m56s" />
+ </test>
+
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="directory-keys" value="/sdcard/CtsHostsideNetworkTests" />
+ <option name="collect-on-run-ended-only" value="true" />
+ </metrics_collector>
+</configuration>
diff --git a/tests/cts/hostside/OWNERS b/tests/cts/hostside/OWNERS
new file mode 100644
index 0000000..52c8053
--- /dev/null
+++ b/tests/cts/hostside/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 61373
+sudheersai@google.com
+lorenzo@google.com
+jchalard@google.com
diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp
new file mode 100644
index 0000000..320a1fa
--- /dev/null
+++ b/tests/cts/hostside/aidl/Android.bp
@@ -0,0 +1,24 @@
+// Copyright (C) 2016 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.
+
+java_test_helper_library {
+ name: "CtsHostsideNetworkTestsAidl",
+ sdk_version: "current",
+ srcs: [
+ "com/android/cts/net/hostside/IMyService.aidl",
+ "com/android/cts/net/hostside/INetworkCallback.aidl",
+ "com/android/cts/net/hostside/INetworkStateObserver.aidl",
+ "com/android/cts/net/hostside/IRemoteSocketFactory.aidl",
+ ],
+}
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
new file mode 100644
index 0000000..5aafdf0
--- /dev/null
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import com.android.cts.net.hostside.INetworkCallback;
+
+interface IMyService {
+ void registerBroadcastReceiver();
+ int getCounters(String receiverName, String action);
+ String checkNetworkStatus();
+ String getRestrictBackgroundStatus();
+ void sendNotification(int notificationId, String notificationType);
+ void registerNetworkCallback(in INetworkCallback cb);
+ void unregisterNetworkCallback();
+}
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl
new file mode 100644
index 0000000..2048bab
--- /dev/null
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.net.hostside;
+
+import android.net.Network;
+import android.net.NetworkCapabilities;
+
+interface INetworkCallback {
+ void onBlockedStatusChanged(in Network network, boolean blocked);
+ void onAvailable(in Network network);
+ void onLost(in Network network);
+ void onCapabilitiesChanged(in Network network, in NetworkCapabilities cap);
+}
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
new file mode 100644
index 0000000..165f530
--- /dev/null
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+interface INetworkStateObserver {
+ boolean isForeground();
+ void onNetworkStateChecked(String resultData);
+}
\ No newline at end of file
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl
new file mode 100644
index 0000000..68176ad
--- /dev/null
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import android.os.ParcelFileDescriptor;
+
+interface IRemoteSocketFactory {
+ ParcelFileDescriptor openSocketFd(String host, int port, int timeoutMs);
+ String getPackageName();
+ int getUid();
+}
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
new file mode 100644
index 0000000..9903756
--- /dev/null
+++ b/tests/cts/hostside/app/Android.bp
@@ -0,0 +1,40 @@
+//
+// Copyright (C) 2014 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.
+//
+
+android_test_helper_app {
+ name: "CtsHostsideNetworkTestsApp",
+ defaults: ["cts_support_defaults"],
+ //sdk_version: "current",
+ platform_apis: true,
+ static_libs: [
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "compatibility-device-util-axt",
+ "ctstestrunner-axt",
+ "ub-uiautomator",
+ "CtsHostsideNetworkTestsAidl",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ srcs: ["src/**/*.java"],
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+}
diff --git a/tests/cts/hostside/app/AndroidManifest.xml b/tests/cts/hostside/app/AndroidManifest.xml
new file mode 100644
index 0000000..3940de4
--- /dev/null
+++ b/tests/cts/hostside/app/AndroidManifest.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.cts.net.hostside">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+
+ <application android:requestLegacyExternalStorage="true" >
+ <uses-library android:name="android.test.runner" />
+ <activity android:name=".MyActivity" />
+ <service android:name=".MyVpnService"
+ android:permission="android.permission.BIND_VPN_SERVICE">
+ <intent-filter>
+ <action android:name="android.net.VpnService"/>
+ </intent-filter>
+ </service>
+ <service
+ android:name=".MyNotificationListenerService"
+ android:label="MyNotificationListenerService"
+ android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" >
+ <intent-filter>
+ <action android:name="android.service.notification.NotificationListenerService" />
+ </intent-filter>
+ </service>
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.cts.net.hostside" />
+
+</manifest>
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java
new file mode 100644
index 0000000..f9e30b6
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
+import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.SystemClock;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Base class for metered and non-metered tests on idle apps.
+ */
+@RequiredProperties({APP_STANDBY_MODE})
+abstract class AbstractAppIdleTestCase extends AbstractRestrictBackgroundNetworkTestCase {
+
+ @Before
+ public final void setUp() throws Exception {
+ super.setUp();
+
+ // Set initial state.
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ setAppIdle(false);
+ turnBatteryOn();
+
+ registerBroadcastReceiver();
+ }
+
+ @After
+ public final void tearDown() throws Exception {
+ super.tearDown();
+
+ executeSilentShellCommand("cmd battery reset");
+ setAppIdle(false);
+ }
+
+ @Test
+ public void testBackgroundNetworkAccess_enabled() throws Exception {
+ setAppIdle(true);
+ assertBackgroundNetworkAccess(false);
+
+ assertsForegroundAlwaysHasNetworkAccess();
+ setAppIdle(true);
+ assertBackgroundNetworkAccess(false);
+
+ // Make sure foreground app doesn't lose access upon enabling it.
+ setAppIdle(true);
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+ finishActivity();
+ assertAppIdle(false); // verify - not idle anymore, since activity was launched...
+ assertBackgroundNetworkAccess(true);
+ setAppIdle(true);
+ assertBackgroundNetworkAccess(false);
+
+ // Same for foreground service.
+ setAppIdle(true);
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
+ stopForegroundService();
+ assertAppIdle(true);
+ assertBackgroundNetworkAccess(false);
+
+ // Set Idle after foreground service start.
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
+ setAppIdle(true);
+ addPowerSaveModeWhitelist(TEST_PKG);
+ removePowerSaveModeWhitelist(TEST_PKG);
+ assertForegroundServiceNetworkAccess();
+ stopForegroundService();
+ assertAppIdle(true);
+ assertBackgroundNetworkAccess(false);
+
+ }
+
+ @Test
+ public void testBackgroundNetworkAccess_whitelisted() throws Exception {
+ setAppIdle(true);
+ assertBackgroundNetworkAccess(false);
+
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertAppIdle(false); // verify - not idle anymore, since whitelisted
+ assertBackgroundNetworkAccess(true);
+
+ setAppIdleNoAssert(true);
+ assertAppIdle(false); // app is still whitelisted
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertAppIdle(true); // verify - idle again, once whitelisted was removed
+ assertBackgroundNetworkAccess(false);
+
+ setAppIdle(true);
+ addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ assertAppIdle(false); // verify - not idle anymore, since whitelisted
+ assertBackgroundNetworkAccess(true);
+
+ setAppIdleNoAssert(true);
+ assertAppIdle(false); // app is still whitelisted
+ removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ assertAppIdle(true); // verify - idle again, once whitelisted was removed
+ assertBackgroundNetworkAccess(false);
+
+ assertsForegroundAlwaysHasNetworkAccess();
+
+ // verify - no whitelist, no access!
+ setAppIdle(true);
+ assertBackgroundNetworkAccess(false);
+ }
+
+ @Test
+ public void testBackgroundNetworkAccess_tempWhitelisted() throws Exception {
+ setAppIdle(true);
+ assertBackgroundNetworkAccess(false);
+
+ addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
+ assertBackgroundNetworkAccess(true);
+ // Wait until the whitelist duration is expired.
+ SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
+ assertBackgroundNetworkAccess(false);
+ }
+
+ @Test
+ public void testBackgroundNetworkAccess_disabled() throws Exception {
+ assertBackgroundNetworkAccess(true);
+
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(true);
+ }
+
+ @RequiredProperties({BATTERY_SAVER_MODE})
+ @Test
+ public void testAppIdleNetworkAccess_whenCharging() throws Exception {
+ // Check that app is paroled when charging
+ setAppIdle(true);
+ assertBackgroundNetworkAccess(false);
+ turnBatteryOff();
+ assertBackgroundNetworkAccess(true);
+ turnBatteryOn();
+ assertBackgroundNetworkAccess(false);
+
+ // Check that app is restricted when not idle but power-save is on
+ setAppIdle(false);
+ assertBackgroundNetworkAccess(true);
+ setBatterySaverMode(true);
+ assertBackgroundNetworkAccess(false);
+ // Use setBatterySaverMode API to leave power-save mode instead of plugging in charger
+ setBatterySaverMode(false);
+ turnBatteryOff();
+ assertBackgroundNetworkAccess(true);
+
+ // And when no longer charging, it still has network access, since it's not idle
+ turnBatteryOn();
+ assertBackgroundNetworkAccess(true);
+ }
+
+ @Test
+ public void testAppIdleNetworkAccess_idleWhitelisted() throws Exception {
+ setAppIdle(true);
+ assertAppIdle(true);
+ assertBackgroundNetworkAccess(false);
+
+ addAppIdleWhitelist(mUid);
+ assertBackgroundNetworkAccess(true);
+
+ removeAppIdleWhitelist(mUid);
+ assertBackgroundNetworkAccess(false);
+
+ // Make sure whitelisting a random app doesn't affect the tested app.
+ addAppIdleWhitelist(mUid + 1);
+ assertBackgroundNetworkAccess(false);
+ removeAppIdleWhitelist(mUid + 1);
+ }
+
+ @Test
+ public void testAppIdle_toast() throws Exception {
+ setAppIdle(true);
+ assertAppIdle(true);
+ assertEquals("Shown", showToast());
+ assertAppIdle(true);
+ // Wait for a couple of seconds for the toast to actually be shown
+ SystemClock.sleep(2000);
+ assertAppIdle(true);
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java
new file mode 100644
index 0000000..04d054d
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Base class for metered and non-metered Battery Saver Mode tests.
+ */
+@RequiredProperties({BATTERY_SAVER_MODE})
+abstract class AbstractBatterySaverModeTestCase extends AbstractRestrictBackgroundNetworkTestCase {
+
+ @Before
+ public final void setUp() throws Exception {
+ super.setUp();
+
+ // Set initial state.
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ setBatterySaverMode(false);
+
+ registerBroadcastReceiver();
+ }
+
+ @After
+ public final void tearDown() throws Exception {
+ super.tearDown();
+
+ setBatterySaverMode(false);
+ }
+
+ @Test
+ public void testBackgroundNetworkAccess_enabled() throws Exception {
+ setBatterySaverMode(true);
+ assertBackgroundNetworkAccess(false);
+
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(false);
+
+ // Make sure foreground app doesn't lose access upon Battery Saver.
+ setBatterySaverMode(false);
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+ setBatterySaverMode(true);
+ assertForegroundNetworkAccess();
+
+ // Although it should not have access while the screen is off.
+ turnScreenOff();
+ assertBackgroundNetworkAccess(false);
+ turnScreenOn();
+ assertForegroundNetworkAccess();
+
+ // Goes back to background state.
+ finishActivity();
+ assertBackgroundNetworkAccess(false);
+
+ // Make sure foreground service doesn't lose access upon enabling Battery Saver.
+ setBatterySaverMode(false);
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
+ setBatterySaverMode(true);
+ assertForegroundNetworkAccess();
+ stopForegroundService();
+ assertBackgroundNetworkAccess(false);
+ }
+
+ @Test
+ public void testBackgroundNetworkAccess_whitelisted() throws Exception {
+ setBatterySaverMode(true);
+ assertBackgroundNetworkAccess(false);
+
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(true);
+
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+
+ addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(true);
+
+ removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(false);
+ }
+
+ @Test
+ public void testBackgroundNetworkAccess_disabled() throws Exception {
+ assertBackgroundNetworkAccess(true);
+
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(true);
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
new file mode 100644
index 0000000..6f32c56
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.Property.DOZE_MODE;
+import static com.android.cts.net.hostside.Property.NOT_LOW_RAM_DEVICE;
+
+import android.os.SystemClock;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Base class for metered and non-metered Doze Mode tests.
+ */
+@RequiredProperties({DOZE_MODE})
+abstract class AbstractDozeModeTestCase extends AbstractRestrictBackgroundNetworkTestCase {
+
+ @Before
+ public final void setUp() throws Exception {
+ super.setUp();
+
+ // Set initial state.
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ setDozeMode(false);
+
+ registerBroadcastReceiver();
+ }
+
+ @After
+ public final void tearDown() throws Exception {
+ super.tearDown();
+
+ setDozeMode(false);
+ }
+
+ @Test
+ public void testBackgroundNetworkAccess_enabled() throws Exception {
+ setDozeMode(true);
+ assertBackgroundNetworkAccess(false);
+
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(false);
+
+ // Make sure foreground service doesn't lose network access upon enabling doze.
+ setDozeMode(false);
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
+ setDozeMode(true);
+ assertForegroundNetworkAccess();
+ stopForegroundService();
+ assertBackgroundState();
+ assertBackgroundNetworkAccess(false);
+ }
+
+ @Test
+ public void testBackgroundNetworkAccess_whitelisted() throws Exception {
+ setDozeMode(true);
+ assertBackgroundNetworkAccess(false);
+
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(true);
+
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+
+ addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+
+ removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(false);
+ }
+
+ @Test
+ public void testBackgroundNetworkAccess_disabled() throws Exception {
+ assertBackgroundNetworkAccess(true);
+
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(true);
+ }
+
+ @RequiredProperties({NOT_LOW_RAM_DEVICE})
+ @Test
+ public void testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction()
+ throws Exception {
+ setPendingIntentWhitelistDuration(NETWORK_TIMEOUT_MS);
+ try {
+ registerNotificationListenerService();
+ setDozeMode(true);
+ assertBackgroundNetworkAccess(false);
+
+ testNotification(4, NOTIFICATION_TYPE_CONTENT);
+ testNotification(8, NOTIFICATION_TYPE_DELETE);
+ testNotification(15, NOTIFICATION_TYPE_FULL_SCREEN);
+ testNotification(16, NOTIFICATION_TYPE_BUNDLE);
+ testNotification(23, NOTIFICATION_TYPE_ACTION);
+ testNotification(42, NOTIFICATION_TYPE_ACTION_BUNDLE);
+ testNotification(108, NOTIFICATION_TYPE_ACTION_REMOTE_INPUT);
+ } finally {
+ resetDeviceIdleSettings();
+ }
+ }
+
+ private void testNotification(int id, String type) throws Exception {
+ sendNotification(id, type);
+ assertBackgroundNetworkAccess(true);
+ if (type.equals(NOTIFICATION_TYPE_ACTION)) {
+ // Make sure access is disabled after it expires. Since this check considerably slows
+ // downs the CTS tests, do it just once.
+ SystemClock.sleep(NETWORK_TIMEOUT_MS);
+ assertBackgroundNetworkAccess(false);
+ }
+ }
+
+ // Must override so it only tests foreground service - once an app goes to foreground, device
+ // leaves Doze Mode.
+ @Override
+ protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception {
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
+ stopForegroundService();
+ assertBackgroundState();
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
new file mode 100644
index 0000000..71f6f2f
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -0,0 +1,880 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
+import static android.os.BatteryManager.BATTERY_PLUGGED_AC;
+import static android.os.BatteryManager.BATTERY_PLUGGED_USB;
+import static android.os.BatteryManager.BATTERY_PLUGGED_WIRELESS;
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.executeShellCommand;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getConnectivityManager;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getContext;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getInstrumentation;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getWifiManager;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.restrictBackgroundValueToString;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.ActivityManager;
+import android.app.Instrumentation;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkInfo.State;
+import android.net.wifi.WifiManager;
+import android.os.BatteryManager;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.service.notification.NotificationListenerService;
+import android.util.Log;
+
+import org.junit.Rule;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Superclass for tests related to background network restrictions.
+ */
+@RunWith(NetworkPolicyTestRunner.class)
+public abstract class AbstractRestrictBackgroundNetworkTestCase {
+ public static final String TAG = "RestrictBackgroundNetworkTests";
+
+ protected static final String TEST_PKG = "com.android.cts.net.hostside";
+ protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
+
+ private static final String TEST_APP2_ACTIVITY_CLASS = TEST_APP2_PKG + ".MyActivity";
+ private static final String TEST_APP2_SERVICE_CLASS = TEST_APP2_PKG + ".MyForegroundService";
+
+ private static final int SLEEP_TIME_SEC = 1;
+
+ // Constants below must match values defined on app2's Common.java
+ private static final String MANIFEST_RECEIVER = "ManifestReceiver";
+ private static final String DYNAMIC_RECEIVER = "DynamicReceiver";
+
+ private static final String ACTION_RECEIVER_READY =
+ "com.android.cts.net.hostside.app2.action.RECEIVER_READY";
+ static final String ACTION_SHOW_TOAST =
+ "com.android.cts.net.hostside.app2.action.SHOW_TOAST";
+
+ protected static final String NOTIFICATION_TYPE_CONTENT = "CONTENT";
+ protected static final String NOTIFICATION_TYPE_DELETE = "DELETE";
+ protected static final String NOTIFICATION_TYPE_FULL_SCREEN = "FULL_SCREEN";
+ protected static final String NOTIFICATION_TYPE_BUNDLE = "BUNDLE";
+ protected static final String NOTIFICATION_TYPE_ACTION = "ACTION";
+ protected static final String NOTIFICATION_TYPE_ACTION_BUNDLE = "ACTION_BUNDLE";
+ protected static final String NOTIFICATION_TYPE_ACTION_REMOTE_INPUT = "ACTION_REMOTE_INPUT";
+
+ // TODO: Update BatteryManager.BATTERY_PLUGGED_ANY as @TestApi
+ public static final int BATTERY_PLUGGED_ANY =
+ BATTERY_PLUGGED_AC | BATTERY_PLUGGED_USB | BATTERY_PLUGGED_WIRELESS;
+
+ private static final String NETWORK_STATUS_SEPARATOR = "\\|";
+ private static final int SECOND_IN_MS = 1000;
+ static final int NETWORK_TIMEOUT_MS = 15 * SECOND_IN_MS;
+ private static int PROCESS_STATE_FOREGROUND_SERVICE;
+
+ private static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
+
+ protected static final int TYPE_COMPONENT_ACTIVTIY = 0;
+ protected static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
+
+ private static final int BATTERY_STATE_TIMEOUT_MS = 5000;
+ private static final int BATTERY_STATE_CHECK_INTERVAL_MS = 500;
+
+ private static final int FOREGROUND_PROC_NETWORK_TIMEOUT_MS = 6000;
+
+ // Must be higher than NETWORK_TIMEOUT_MS
+ private static final int ORDERED_BROADCAST_TIMEOUT_MS = NETWORK_TIMEOUT_MS * 4;
+
+ private static final IntentFilter BATTERY_CHANGED_FILTER =
+ new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+
+ private static final String APP_NOT_FOREGROUND_ERROR = "app_not_fg";
+
+ protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 5_000; // 5 sec
+
+ protected Context mContext;
+ protected Instrumentation mInstrumentation;
+ protected ConnectivityManager mCm;
+ protected int mUid;
+ private int mMyUid;
+ private MyServiceClient mServiceClient;
+ private String mDeviceIdleConstantsSetting;
+
+ @Rule
+ public final RuleChain mRuleChain = RuleChain.outerRule(new RequiredPropertiesRule())
+ .around(new MeterednessConfigurationRule());
+
+ protected void setUp() throws Exception {
+
+ PROCESS_STATE_FOREGROUND_SERVICE = (Integer) ActivityManager.class
+ .getDeclaredField("PROCESS_STATE_FOREGROUND_SERVICE").get(null);
+ mInstrumentation = getInstrumentation();
+ mContext = getContext();
+ mCm = getConnectivityManager();
+ mUid = getUid(TEST_APP2_PKG);
+ mMyUid = getUid(mContext.getPackageName());
+ mServiceClient = new MyServiceClient(mContext);
+ mServiceClient.bind();
+ mDeviceIdleConstantsSetting = "device_idle_constants";
+ executeShellCommand("cmd netpolicy start-watching " + mUid);
+ setAppIdle(false);
+
+ Log.i(TAG, "Apps status:\n"
+ + "\ttest app: uid=" + mMyUid + ", state=" + getProcessStateByUid(mMyUid) + "\n"
+ + "\tapp2: uid=" + mUid + ", state=" + getProcessStateByUid(mUid));
+ }
+
+ protected void tearDown() throws Exception {
+ executeShellCommand("cmd netpolicy stop-watching");
+ mServiceClient.unbind();
+ }
+
+ protected int getUid(String packageName) throws Exception {
+ return mContext.getPackageManager().getPackageUid(packageName, 0);
+ }
+
+ protected void assertRestrictBackgroundChangedReceived(int expectedCount) throws Exception {
+ assertRestrictBackgroundChangedReceived(DYNAMIC_RECEIVER, expectedCount);
+ assertRestrictBackgroundChangedReceived(MANIFEST_RECEIVER, 0);
+ }
+
+ protected void assertRestrictBackgroundChangedReceived(String receiverName, int expectedCount)
+ throws Exception {
+ int attempts = 0;
+ int count = 0;
+ final int maxAttempts = 5;
+ do {
+ attempts++;
+ count = getNumberBroadcastsReceived(receiverName, ACTION_RESTRICT_BACKGROUND_CHANGED);
+ assertFalse("Expected count " + expectedCount + " but actual is " + count,
+ count > expectedCount);
+ if (count == expectedCount) {
+ break;
+ }
+ Log.d(TAG, "Expecting count " + expectedCount + " but actual is " + count + " after "
+ + attempts + " attempts; sleeping "
+ + SLEEP_TIME_SEC + " seconds before trying again");
+ SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
+ } while (attempts <= maxAttempts);
+ assertEquals("Number of expected broadcasts for " + receiverName + " not reached after "
+ + maxAttempts * SLEEP_TIME_SEC + " seconds", expectedCount, count);
+ }
+
+ protected String sendOrderedBroadcast(Intent intent) throws Exception {
+ return sendOrderedBroadcast(intent, ORDERED_BROADCAST_TIMEOUT_MS);
+ }
+
+ protected String sendOrderedBroadcast(Intent intent, int timeoutMs) throws Exception {
+ final LinkedBlockingQueue<String> result = new LinkedBlockingQueue<>(1);
+ Log.d(TAG, "Sending ordered broadcast: " + intent);
+ mContext.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String resultData = getResultData();
+ if (resultData == null) {
+ Log.e(TAG, "Received null data from ordered intent");
+ return;
+ }
+ result.offer(resultData);
+ }
+ }, null, 0, null, null);
+
+ final String resultData = result.poll(timeoutMs, TimeUnit.MILLISECONDS);
+ Log.d(TAG, "Ordered broadcast response after " + timeoutMs + "ms: " + resultData );
+ return resultData;
+ }
+
+ protected int getNumberBroadcastsReceived(String receiverName, String action) throws Exception {
+ return mServiceClient.getCounters(receiverName, action);
+ }
+
+ protected void assertRestrictBackgroundStatus(int expectedStatus) throws Exception {
+ final String status = mServiceClient.getRestrictBackgroundStatus();
+ assertNotNull("didn't get API status from app2", status);
+ assertEquals(restrictBackgroundValueToString(expectedStatus),
+ restrictBackgroundValueToString(Integer.parseInt(status)));
+ }
+
+ protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
+ assertBackgroundState();
+ assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */);
+ }
+
+ protected void assertForegroundNetworkAccess() throws Exception {
+ assertForegroundState();
+ // We verified that app is in foreground state but if the screen turns-off while
+ // verifying for network access, the app will go into background state (in case app's
+ // foreground status was due to top activity). So, turn the screen on when verifying
+ // network connectivity.
+ assertNetworkAccess(true /* expectAvailable */, true /* needScreenOn */);
+ }
+
+ protected void assertForegroundServiceNetworkAccess() throws Exception {
+ assertForegroundServiceState();
+ assertNetworkAccess(true /* expectAvailable */, false /* needScreenOn */);
+ }
+
+ /**
+ * Asserts that an app always have access while on foreground or running a foreground service.
+ *
+ * <p>This method will launch an activity and a foreground service to make the assertion, but
+ * will finish the activity / stop the service afterwards.
+ */
+ protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception{
+ // Checks foreground first.
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+ finishActivity();
+
+ // Then foreground service
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
+ stopForegroundService();
+ }
+
+ protected final void assertBackgroundState() throws Exception {
+ final int maxTries = 30;
+ ProcessState state = null;
+ for (int i = 1; i <= maxTries; i++) {
+ state = getProcessStateByUid(mUid);
+ Log.v(TAG, "assertBackgroundState(): status for app2 (" + mUid + ") on attempt #" + i
+ + ": " + state);
+ if (isBackground(state.state)) {
+ return;
+ }
+ Log.d(TAG, "App not on background state (" + state + ") on attempt #" + i
+ + "; sleeping 1s before trying again");
+ SystemClock.sleep(SECOND_IN_MS);
+ }
+ fail("App2 is not on background state after " + maxTries + " attempts: " + state );
+ }
+
+ protected final void assertForegroundState() throws Exception {
+ final int maxTries = 30;
+ ProcessState state = null;
+ for (int i = 1; i <= maxTries; i++) {
+ state = getProcessStateByUid(mUid);
+ Log.v(TAG, "assertForegroundState(): status for app2 (" + mUid + ") on attempt #" + i
+ + ": " + state);
+ if (!isBackground(state.state)) {
+ return;
+ }
+ Log.d(TAG, "App not on foreground state on attempt #" + i
+ + "; sleeping 1s before trying again");
+ turnScreenOn();
+ SystemClock.sleep(SECOND_IN_MS);
+ }
+ fail("App2 is not on foreground state after " + maxTries + " attempts: " + state );
+ }
+
+ protected final void assertForegroundServiceState() throws Exception {
+ final int maxTries = 30;
+ ProcessState state = null;
+ for (int i = 1; i <= maxTries; i++) {
+ state = getProcessStateByUid(mUid);
+ Log.v(TAG, "assertForegroundServiceState(): status for app2 (" + mUid + ") on attempt #"
+ + i + ": " + state);
+ if (state.state == PROCESS_STATE_FOREGROUND_SERVICE) {
+ return;
+ }
+ Log.d(TAG, "App not on foreground service state on attempt #" + i
+ + "; sleeping 1s before trying again");
+ SystemClock.sleep(SECOND_IN_MS);
+ }
+ fail("App2 is not on foreground service state after " + maxTries + " attempts: " + state );
+ }
+
+ /**
+ * Returns whether an app state should be considered "background" for restriction purposes.
+ */
+ protected boolean isBackground(int state) {
+ return state > PROCESS_STATE_FOREGROUND_SERVICE;
+ }
+
+ /**
+ * Asserts whether the active network is available or not.
+ */
+ private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn)
+ throws Exception {
+ final int maxTries = 5;
+ String error = null;
+ int timeoutMs = 500;
+
+ for (int i = 1; i <= maxTries; i++) {
+ error = checkNetworkAccess(expectAvailable);
+
+ if (error.isEmpty()) return;
+
+ // TODO: ideally, it should retry only when it cannot connect to an external site,
+ // or no retry at all! But, currently, the initial change fails almost always on
+ // battery saver tests because the netd changes are made asynchronously.
+ // Once b/27803922 is fixed, this retry mechanism should be revisited.
+
+ Log.w(TAG, "Network status didn't match for expectAvailable=" + expectAvailable
+ + " on attempt #" + i + ": " + error + "\n"
+ + "Sleeping " + timeoutMs + "ms before trying again");
+ if (needScreenOn) {
+ turnScreenOn();
+ }
+ // No sleep after the last turn
+ if (i < maxTries) {
+ SystemClock.sleep(timeoutMs);
+ }
+ // Exponential back-off.
+ timeoutMs = Math.min(timeoutMs*2, NETWORK_TIMEOUT_MS);
+ }
+ fail("Invalid state for expectAvailable=" + expectAvailable + " after " + maxTries
+ + " attempts.\nLast error: " + error);
+ }
+
+ /**
+ * Checks whether the network is available as expected.
+ *
+ * @return error message with the mismatch (or empty if assertion passed).
+ */
+ private String checkNetworkAccess(boolean expectAvailable) throws Exception {
+ final String resultData = mServiceClient.checkNetworkStatus();
+ return checkForAvailabilityInResultData(resultData, expectAvailable);
+ }
+
+ private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable) {
+ if (resultData == null) {
+ assertNotNull("Network status from app2 is null", resultData);
+ }
+ // Network status format is described on MyBroadcastReceiver.checkNetworkStatus()
+ final String[] parts = resultData.split(NETWORK_STATUS_SEPARATOR);
+ assertEquals("Wrong network status: " + resultData, 5, parts.length);
+ final State state = parts[0].equals("null") ? null : State.valueOf(parts[0]);
+ final DetailedState detailedState = parts[1].equals("null")
+ ? null : DetailedState.valueOf(parts[1]);
+ final boolean connected = Boolean.valueOf(parts[2]);
+ final String connectionCheckDetails = parts[3];
+ final String networkInfo = parts[4];
+
+ final StringBuilder errors = new StringBuilder();
+ final State expectedState;
+ final DetailedState expectedDetailedState;
+ if (expectAvailable) {
+ expectedState = State.CONNECTED;
+ expectedDetailedState = DetailedState.CONNECTED;
+ } else {
+ expectedState = State.DISCONNECTED;
+ expectedDetailedState = DetailedState.BLOCKED;
+ }
+
+ if (expectAvailable != connected) {
+ errors.append(String.format("External site connection failed: expected %s, got %s\n",
+ expectAvailable, connected));
+ }
+ if (expectedState != state || expectedDetailedState != detailedState) {
+ errors.append(String.format("Connection state mismatch: expected %s/%s, got %s/%s\n",
+ expectedState, expectedDetailedState, state, detailedState));
+ }
+
+ if (errors.length() > 0) {
+ errors.append("\tnetworkInfo: " + networkInfo + "\n");
+ errors.append("\tconnectionCheckDetails: " + connectionCheckDetails + "\n");
+ }
+ return errors.toString();
+ }
+
+ /**
+ * Runs a Shell command which is not expected to generate output.
+ */
+ protected void executeSilentShellCommand(String command) {
+ final String result = executeShellCommand(command);
+ assertTrue("Command '" + command + "' failed: " + result, result.trim().isEmpty());
+ }
+
+ /**
+ * Asserts the result of a command, wait and re-running it a couple times if necessary.
+ */
+ protected void assertDelayedShellCommand(String command, final String expectedResult)
+ throws Exception {
+ assertDelayedShellCommand(command, 5, 1, expectedResult);
+ }
+
+ protected void assertDelayedShellCommand(String command, int maxTries, int napTimeSeconds,
+ final String expectedResult) throws Exception {
+ assertDelayedShellCommand(command, maxTries, napTimeSeconds, new ExpectResultChecker() {
+
+ @Override
+ public boolean isExpected(String result) {
+ return expectedResult.equals(result);
+ }
+
+ @Override
+ public String getExpected() {
+ return expectedResult;
+ }
+ });
+ }
+
+ protected void assertDelayedShellCommand(String command, int maxTries, int napTimeSeconds,
+ ExpectResultChecker checker) throws Exception {
+ String result = "";
+ for (int i = 1; i <= maxTries; i++) {
+ result = executeShellCommand(command).trim();
+ if (checker.isExpected(result)) return;
+ Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
+ + checker.getExpected() + "' on attempt #" + i
+ + "; sleeping " + napTimeSeconds + "s before trying again");
+ SystemClock.sleep(napTimeSeconds * SECOND_IN_MS);
+ }
+ fail("Command '" + command + "' did not return '" + checker.getExpected() + "' after "
+ + maxTries
+ + " attempts. Last result: '" + result + "'");
+ }
+
+ protected void addRestrictBackgroundWhitelist(int uid) throws Exception {
+ executeShellCommand("cmd netpolicy add restrict-background-whitelist " + uid);
+ assertRestrictBackgroundWhitelist(uid, true);
+ // UID policies live by the Highlander rule: "There can be only one".
+ // Hence, if app is whitelisted, it should not be blacklisted.
+ assertRestrictBackgroundBlacklist(uid, false);
+ }
+
+ protected void removeRestrictBackgroundWhitelist(int uid) throws Exception {
+ executeShellCommand("cmd netpolicy remove restrict-background-whitelist " + uid);
+ assertRestrictBackgroundWhitelist(uid, false);
+ }
+
+ protected void assertRestrictBackgroundWhitelist(int uid, boolean expected) throws Exception {
+ assertRestrictBackground("restrict-background-whitelist", uid, expected);
+ }
+
+ protected void addRestrictBackgroundBlacklist(int uid) throws Exception {
+ executeShellCommand("cmd netpolicy add restrict-background-blacklist " + uid);
+ assertRestrictBackgroundBlacklist(uid, true);
+ // UID policies live by the Highlander rule: "There can be only one".
+ // Hence, if app is blacklisted, it should not be whitelisted.
+ assertRestrictBackgroundWhitelist(uid, false);
+ }
+
+ protected void removeRestrictBackgroundBlacklist(int uid) throws Exception {
+ executeShellCommand("cmd netpolicy remove restrict-background-blacklist " + uid);
+ assertRestrictBackgroundBlacklist(uid, false);
+ }
+
+ protected void assertRestrictBackgroundBlacklist(int uid, boolean expected) throws Exception {
+ assertRestrictBackground("restrict-background-blacklist", uid, expected);
+ }
+
+ protected void addAppIdleWhitelist(int uid) throws Exception {
+ executeShellCommand("cmd netpolicy add app-idle-whitelist " + uid);
+ assertAppIdleWhitelist(uid, true);
+ }
+
+ protected void removeAppIdleWhitelist(int uid) throws Exception {
+ executeShellCommand("cmd netpolicy remove app-idle-whitelist " + uid);
+ assertAppIdleWhitelist(uid, false);
+ }
+
+ protected void assertAppIdleWhitelist(int uid, boolean expected) throws Exception {
+ assertRestrictBackground("app-idle-whitelist", uid, expected);
+ }
+
+ private void assertRestrictBackground(String list, int uid, boolean expected) throws Exception {
+ final int maxTries = 5;
+ boolean actual = false;
+ final String expectedUid = Integer.toString(uid);
+ String uids = "";
+ for (int i = 1; i <= maxTries; i++) {
+ final String output =
+ executeShellCommand("cmd netpolicy list " + list);
+ uids = output.split(":")[1];
+ for (String candidate : uids.split(" ")) {
+ actual = candidate.trim().equals(expectedUid);
+ if (expected == actual) {
+ return;
+ }
+ }
+ Log.v(TAG, list + " check for uid " + uid + " doesn't match yet (expected "
+ + expected + ", got " + actual + "); sleeping 1s before polling again");
+ SystemClock.sleep(SECOND_IN_MS);
+ }
+ fail(list + " check for uid " + uid + " failed: expected " + expected + ", got " + actual
+ + ". Full list: " + uids);
+ }
+
+ protected void addTempPowerSaveModeWhitelist(String packageName, long duration)
+ throws Exception {
+ Log.i(TAG, "Adding pkg " + packageName + " to temp-power-save-mode whitelist");
+ executeShellCommand("dumpsys deviceidle tempwhitelist -d " + duration + " " + packageName);
+ }
+
+ protected void assertPowerSaveModeWhitelist(String packageName, boolean expected)
+ throws Exception {
+ // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
+ // need to use netpolicy for whitelisting
+ assertDelayedShellCommand("dumpsys deviceidle whitelist =" + packageName,
+ Boolean.toString(expected));
+ }
+
+ protected void addPowerSaveModeWhitelist(String packageName) throws Exception {
+ Log.i(TAG, "Adding package " + packageName + " to power-save-mode whitelist");
+ // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
+ // need to use netpolicy for whitelisting
+ executeShellCommand("dumpsys deviceidle whitelist +" + packageName);
+ assertPowerSaveModeWhitelist(packageName, true);
+ }
+
+ protected void removePowerSaveModeWhitelist(String packageName) throws Exception {
+ Log.i(TAG, "Removing package " + packageName + " from power-save-mode whitelist");
+ // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
+ // need to use netpolicy for whitelisting
+ executeShellCommand("dumpsys deviceidle whitelist -" + packageName);
+ assertPowerSaveModeWhitelist(packageName, false);
+ }
+
+ protected void assertPowerSaveModeExceptIdleWhitelist(String packageName, boolean expected)
+ throws Exception {
+ // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
+ // need to use netpolicy for whitelisting
+ assertDelayedShellCommand("dumpsys deviceidle except-idle-whitelist =" + packageName,
+ Boolean.toString(expected));
+ }
+
+ protected void addPowerSaveModeExceptIdleWhitelist(String packageName) throws Exception {
+ Log.i(TAG, "Adding package " + packageName + " to power-save-mode-except-idle whitelist");
+ // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
+ // need to use netpolicy for whitelisting
+ executeShellCommand("dumpsys deviceidle except-idle-whitelist +" + packageName);
+ assertPowerSaveModeExceptIdleWhitelist(packageName, true);
+ }
+
+ protected void removePowerSaveModeExceptIdleWhitelist(String packageName) throws Exception {
+ Log.i(TAG, "Removing package " + packageName
+ + " from power-save-mode-except-idle whitelist");
+ // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
+ // need to use netpolicy for whitelisting
+ executeShellCommand("dumpsys deviceidle except-idle-whitelist reset");
+ assertPowerSaveModeExceptIdleWhitelist(packageName, false);
+ }
+
+ protected void turnBatteryOn() throws Exception {
+ executeSilentShellCommand("cmd battery unplug");
+ executeSilentShellCommand("cmd battery set status "
+ + BatteryManager.BATTERY_STATUS_DISCHARGING);
+ assertBatteryState(false);
+ }
+
+ protected void turnBatteryOff() throws Exception {
+ executeSilentShellCommand("cmd battery set ac " + BATTERY_PLUGGED_ANY);
+ executeSilentShellCommand("cmd battery set level 100");
+ executeSilentShellCommand("cmd battery set status "
+ + BatteryManager.BATTERY_STATUS_CHARGING);
+ assertBatteryState(true);
+ }
+
+ private void assertBatteryState(boolean pluggedIn) throws Exception {
+ final long endTime = SystemClock.elapsedRealtime() + BATTERY_STATE_TIMEOUT_MS;
+ while (isDevicePluggedIn() != pluggedIn && SystemClock.elapsedRealtime() <= endTime) {
+ Thread.sleep(BATTERY_STATE_CHECK_INTERVAL_MS);
+ }
+ if (isDevicePluggedIn() != pluggedIn) {
+ fail("Timed out waiting for the plugged-in state to change,"
+ + " expected pluggedIn: " + pluggedIn);
+ }
+ }
+
+ private boolean isDevicePluggedIn() {
+ final Intent batteryIntent = mContext.registerReceiver(null, BATTERY_CHANGED_FILTER);
+ return batteryIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) > 0;
+ }
+
+ protected void turnScreenOff() throws Exception {
+ executeSilentShellCommand("input keyevent KEYCODE_SLEEP");
+ }
+
+ protected void turnScreenOn() throws Exception {
+ executeSilentShellCommand("input keyevent KEYCODE_WAKEUP");
+ executeSilentShellCommand("wm dismiss-keyguard");
+ }
+
+ protected void setBatterySaverMode(boolean enabled) throws Exception {
+ Log.i(TAG, "Setting Battery Saver Mode to " + enabled);
+ if (enabled) {
+ turnBatteryOn();
+ executeSilentShellCommand("cmd power set-mode 1");
+ } else {
+ executeSilentShellCommand("cmd power set-mode 0");
+ turnBatteryOff();
+ }
+ }
+
+ protected void setDozeMode(boolean enabled) throws Exception {
+ // Check doze mode is supported.
+ assertTrue("Device does not support Doze Mode", isDozeModeSupported());
+
+ Log.i(TAG, "Setting Doze Mode to " + enabled);
+ if (enabled) {
+ turnBatteryOn();
+ turnScreenOff();
+ executeShellCommand("dumpsys deviceidle force-idle deep");
+ } else {
+ turnScreenOn();
+ turnBatteryOff();
+ executeShellCommand("dumpsys deviceidle unforce");
+ }
+ assertDozeMode(enabled);
+ }
+
+ protected void assertDozeMode(boolean enabled) throws Exception {
+ assertDelayedShellCommand("dumpsys deviceidle get deep", enabled ? "IDLE" : "ACTIVE");
+ }
+
+ protected void setAppIdle(boolean enabled) throws Exception {
+ Log.i(TAG, "Setting app idle to " + enabled);
+ executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled );
+ assertAppIdle(enabled);
+ }
+
+ protected void setAppIdleNoAssert(boolean enabled) throws Exception {
+ Log.i(TAG, "Setting app idle to " + enabled);
+ executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled );
+ }
+
+ protected void assertAppIdle(boolean enabled) throws Exception {
+ try {
+ assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG, 15, 2, "Idle=" + enabled);
+ } catch (Throwable e) {
+ throw e;
+ }
+ }
+
+ /**
+ * Starts a service that will register a broadcast receiver to receive
+ * {@code RESTRICT_BACKGROUND_CHANGE} intents.
+ * <p>
+ * The service must run in a separate app because otherwise it would be killed every time
+ * {@link #runDeviceTests(String, String)} is executed.
+ */
+ protected void registerBroadcastReceiver() throws Exception {
+ mServiceClient.registerBroadcastReceiver();
+
+ final Intent intent = new Intent(ACTION_RECEIVER_READY)
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ // Wait until receiver is ready.
+ final int maxTries = 10;
+ for (int i = 1; i <= maxTries; i++) {
+ final String message = sendOrderedBroadcast(intent, SECOND_IN_MS * 4);
+ Log.d(TAG, "app2 receiver acked: " + message);
+ if (message != null) {
+ return;
+ }
+ Log.v(TAG, "app2 receiver is not ready yet; sleeping 1s before polling again");
+ SystemClock.sleep(SECOND_IN_MS);
+ }
+ fail("app2 receiver is not ready");
+ }
+
+ protected void registerNetworkCallback(INetworkCallback cb) throws Exception {
+ mServiceClient.registerNetworkCallback(cb);
+ }
+
+ protected void unregisterNetworkCallback() throws Exception {
+ mServiceClient.unregisterNetworkCallback();
+ }
+
+ /**
+ * Registers a {@link NotificationListenerService} implementation that will execute the
+ * notification actions right after the notification is sent.
+ */
+ protected void registerNotificationListenerService() throws Exception {
+ executeShellCommand("cmd notification allow_listener "
+ + MyNotificationListenerService.getId());
+ final NotificationManager nm = mContext.getSystemService(NotificationManager.class);
+ final ComponentName listenerComponent = MyNotificationListenerService.getComponentName();
+ assertTrue(listenerComponent + " has not been granted access",
+ nm.isNotificationListenerAccessGranted(listenerComponent));
+ }
+
+ protected void setPendingIntentWhitelistDuration(int durationMs) throws Exception {
+ executeSilentShellCommand(String.format(
+ "settings put global %s %s=%d", mDeviceIdleConstantsSetting,
+ "notification_whitelist_duration", durationMs));
+ }
+
+ protected void resetDeviceIdleSettings() throws Exception {
+ executeShellCommand(String.format("settings delete global %s",
+ mDeviceIdleConstantsSetting));
+ }
+
+ protected void launchComponentAndAssertNetworkAccess(int type) throws Exception {
+ if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
+ startForegroundService();
+ assertForegroundServiceNetworkAccess();
+ return;
+ } else if (type == TYPE_COMPONENT_ACTIVTIY) {
+ turnScreenOn();
+ // Wait for screen-on state to propagate through the system.
+ SystemClock.sleep(2000);
+ final CountDownLatch latch = new CountDownLatch(1);
+ final Intent launchIntent = getIntentForComponent(type);
+ final Bundle extras = new Bundle();
+ final String[] errors = new String[]{null};
+ extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, errors));
+ launchIntent.putExtras(extras);
+ mContext.startActivity(launchIntent);
+ if (latch.await(FOREGROUND_PROC_NETWORK_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ if (!errors[0].isEmpty()) {
+ if (errors[0] == APP_NOT_FOREGROUND_ERROR) {
+ // App didn't come to foreground when the activity is started, so try again.
+ assertForegroundNetworkAccess();
+ } else {
+ fail("Network is not available for app2 (" + mUid + "): " + errors[0]);
+ }
+ }
+ } else {
+ fail("Timed out waiting for network availability status from app2 (" + mUid + ")");
+ }
+ } else {
+ throw new IllegalArgumentException("Unknown type: " + type);
+ }
+ }
+
+ private void startForegroundService() throws Exception {
+ final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_FOREGROUND_SERVICE);
+ mContext.startForegroundService(launchIntent);
+ assertForegroundServiceState();
+ }
+
+ private Intent getIntentForComponent(int type) {
+ final Intent intent = new Intent();
+ if (type == TYPE_COMPONENT_ACTIVTIY) {
+ intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_ACTIVITY_CLASS))
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ } else if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
+ intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS))
+ .setFlags(1);
+ } else {
+ fail("Unknown type: " + type);
+ }
+ return intent;
+ }
+
+ protected void stopForegroundService() throws Exception {
+ executeShellCommand(String.format("am startservice -f 2 %s/%s",
+ TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS));
+ // NOTE: cannot assert state because it depends on whether activity was on top before.
+ }
+
+ private Binder getNewNetworkStateObserver(final CountDownLatch latch,
+ final String[] errors) {
+ return new INetworkStateObserver.Stub() {
+ @Override
+ public boolean isForeground() {
+ try {
+ final ProcessState state = getProcessStateByUid(mUid);
+ return !isBackground(state.state);
+ } catch (Exception e) {
+ Log.d(TAG, "Error while reading the proc state for " + mUid + ": " + e);
+ return false;
+ }
+ }
+
+ @Override
+ public void onNetworkStateChecked(String resultData) {
+ errors[0] = resultData == null
+ ? APP_NOT_FOREGROUND_ERROR
+ : checkForAvailabilityInResultData(resultData, true);
+ latch.countDown();
+ }
+ };
+ }
+
+ /**
+ * Finishes an activity on app2 so its process is demoted fromforeground status.
+ */
+ protected void finishActivity() throws Exception {
+ executeShellCommand("am broadcast -a "
+ + " com.android.cts.net.hostside.app2.action.FINISH_ACTIVITY "
+ + "--receiver-foreground --receiver-registered-only");
+ }
+
+ protected void sendNotification(int notificationId, String notificationType) throws Exception {
+ Log.d(TAG, "Sending notification broadcast (id=" + notificationId
+ + ", type=" + notificationType);
+ mServiceClient.sendNotification(notificationId, notificationType);
+ }
+
+ protected String showToast() {
+ final Intent intent = new Intent(ACTION_SHOW_TOAST);
+ intent.setPackage(TEST_APP2_PKG);
+ Log.d(TAG, "Sending request to show toast");
+ try {
+ return sendOrderedBroadcast(intent, 3 * SECOND_IN_MS);
+ } catch (Exception e) {
+ return "";
+ }
+ }
+
+ private ProcessState getProcessStateByUid(int uid) throws Exception {
+ return new ProcessState(executeShellCommand("cmd activity get-uid-state " + uid));
+ }
+
+ private static class ProcessState {
+ private final String fullState;
+ final int state;
+
+ ProcessState(String fullState) {
+ this.fullState = fullState;
+ try {
+ this.state = Integer.parseInt(fullState.split(" ")[0]);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Could not parse " + fullState);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return fullState;
+ }
+ }
+
+ /**
+ * Helper class used to assert the result of a Shell command.
+ */
+ protected static interface ExpectResultChecker {
+ /**
+ * Checkes whether the result of the command matched the expectation.
+ */
+ boolean isExpected(String result);
+ /**
+ * Gets the expected result so it's displayed on log and failure messages.
+ */
+ String getExpected();
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleMeteredTest.java
new file mode 100644
index 0000000..f1858d6
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class AppIdleMeteredTest extends AbstractAppIdleTestCase {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleNonMeteredTest.java
new file mode 100644
index 0000000..e737a6d
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleNonMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class AppIdleNonMeteredTest extends AbstractAppIdleTestCase {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeMeteredTest.java
new file mode 100644
index 0000000..c78ca2e
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class BatterySaverModeMeteredTest extends AbstractBatterySaverModeTestCase {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeNonMeteredTest.java
new file mode 100644
index 0000000..fb52a54
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeNonMeteredTest.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class BatterySaverModeNonMeteredTest extends AbstractBatterySaverModeTestCase {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
new file mode 100644
index 0000000..604a0b6
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
+import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
+import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+import static com.android.cts.net.hostside.Property.NO_DATA_SAVER_MODE;
+
+import static org.junit.Assert.fail;
+
+import com.android.compatibility.common.util.CddTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import androidx.test.filters.LargeTest;
+
+@RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
+@LargeTest
+public class DataSaverModeTest extends AbstractRestrictBackgroundNetworkTestCase {
+
+ private static final String[] REQUIRED_WHITELISTED_PACKAGES = {
+ "com.android.providers.downloads"
+ };
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ // Set initial state.
+ setRestrictBackground(false);
+ removeRestrictBackgroundWhitelist(mUid);
+ removeRestrictBackgroundBlacklist(mUid);
+
+ registerBroadcastReceiver();
+ assertRestrictBackgroundChangedReceived(0);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ setRestrictBackground(false);
+ }
+
+ @Test
+ public void testGetRestrictBackgroundStatus_disabled() throws Exception {
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
+
+ // Verify status is always disabled, never whitelisted
+ addRestrictBackgroundWhitelist(mUid);
+ assertRestrictBackgroundChangedReceived(0);
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
+
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
+ }
+
+ @Test
+ public void testGetRestrictBackgroundStatus_whitelisted() throws Exception {
+ setRestrictBackground(true);
+ assertRestrictBackgroundChangedReceived(1);
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
+
+ addRestrictBackgroundWhitelist(mUid);
+ assertRestrictBackgroundChangedReceived(2);
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_WHITELISTED);
+
+ removeRestrictBackgroundWhitelist(mUid);
+ assertRestrictBackgroundChangedReceived(3);
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
+
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
+ }
+
+ @Test
+ public void testGetRestrictBackgroundStatus_enabled() throws Exception {
+ setRestrictBackground(true);
+ assertRestrictBackgroundChangedReceived(1);
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
+
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
+
+ // Make sure foreground app doesn't lose access upon enabling Data Saver.
+ setRestrictBackground(false);
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+ setRestrictBackground(true);
+ assertForegroundNetworkAccess();
+
+ // Although it should not have access while the screen is off.
+ turnScreenOff();
+ assertBackgroundNetworkAccess(false);
+ turnScreenOn();
+ assertForegroundNetworkAccess();
+
+ // Goes back to background state.
+ finishActivity();
+ assertBackgroundNetworkAccess(false);
+
+ // Make sure foreground service doesn't lose access upon enabling Data Saver.
+ setRestrictBackground(false);
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
+ setRestrictBackground(true);
+ assertForegroundNetworkAccess();
+ stopForegroundService();
+ assertBackgroundNetworkAccess(false);
+ }
+
+ @Test
+ public void testGetRestrictBackgroundStatus_blacklisted() throws Exception {
+ addRestrictBackgroundBlacklist(mUid);
+ assertRestrictBackgroundChangedReceived(1);
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
+
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertRestrictBackgroundChangedReceived(1);
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
+
+ // UID policies live by the Highlander rule: "There can be only one".
+ // Hence, if app is whitelisted, it should not be blacklisted anymore.
+ setRestrictBackground(true);
+ assertRestrictBackgroundChangedReceived(2);
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
+ addRestrictBackgroundWhitelist(mUid);
+ assertRestrictBackgroundChangedReceived(3);
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_WHITELISTED);
+
+ // Check status after removing blacklist.
+ // ...re-enables first
+ addRestrictBackgroundBlacklist(mUid);
+ assertRestrictBackgroundChangedReceived(4);
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
+ assertsForegroundAlwaysHasNetworkAccess();
+ // ... remove blacklist - access's still rejected because Data Saver is on
+ removeRestrictBackgroundBlacklist(mUid);
+ assertRestrictBackgroundChangedReceived(4);
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
+ assertsForegroundAlwaysHasNetworkAccess();
+ // ... finally, disable Data Saver
+ setRestrictBackground(false);
+ assertRestrictBackgroundChangedReceived(5);
+ assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
+ assertsForegroundAlwaysHasNetworkAccess();
+ }
+
+ @Test
+ public void testGetRestrictBackgroundStatus_requiredWhitelistedPackages() throws Exception {
+ final StringBuilder error = new StringBuilder();
+ for (String packageName : REQUIRED_WHITELISTED_PACKAGES) {
+ int uid = -1;
+ try {
+ uid = getUid(packageName);
+ assertRestrictBackgroundWhitelist(uid, true);
+ } catch (Throwable t) {
+ error.append("\nFailed for '").append(packageName).append("'");
+ if (uid > 0) {
+ error.append(" (uid ").append(uid).append(")");
+ }
+ error.append(": ").append(t).append("\n");
+ }
+ }
+ if (error.length() > 0) {
+ fail(error.toString());
+ }
+ }
+
+ @RequiredProperties({NO_DATA_SAVER_MODE})
+ @CddTest(requirement="7.4.7/C-2-2")
+ @Test
+ public void testBroadcastNotSentOnUnsupportedDevices() throws Exception {
+ setRestrictBackground(true);
+ assertRestrictBackgroundChangedReceived(0);
+
+ setRestrictBackground(false);
+ assertRestrictBackgroundChangedReceived(0);
+
+ setRestrictBackground(true);
+ assertRestrictBackgroundChangedReceived(0);
+ }
+
+ private void assertDataSaverStatusOnBackground(int expectedStatus) throws Exception {
+ assertRestrictBackgroundStatus(expectedStatus);
+ assertBackgroundNetworkAccess(expectedStatus != RESTRICT_BACKGROUND_STATUS_ENABLED);
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeMeteredTest.java
new file mode 100644
index 0000000..4306c99
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class DozeModeMeteredTest extends AbstractDozeModeTestCase {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeNonMeteredTest.java
new file mode 100644
index 0000000..1e89f15
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeNonMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class DozeModeNonMeteredTest extends AbstractDozeModeTestCase {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
new file mode 100644
index 0000000..5ecb399
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
+import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TEST_APP2_PKG;
+import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TEST_PKG;
+
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import com.android.compatibility.common.util.OnFailureRule;
+
+import org.junit.AssumptionViolatedException;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+public class DumpOnFailureRule extends OnFailureRule {
+ private File mDumpDir = new File(Environment.getExternalStorageDirectory(),
+ "CtsHostsideNetworkTests");
+
+ @Override
+ public void onTestFailure(Statement base, Description description, Throwable throwable) {
+ final String testName = description.getClassName() + "_" + description.getMethodName();
+
+ if (throwable instanceof AssumptionViolatedException) {
+ Log.d(TAG, "Skipping test " + testName + ": " + throwable);
+ return;
+ }
+
+ prepareDumpRootDir();
+ final File dumpFile = new File(mDumpDir, "dump-" + testName);
+ Log.i(TAG, "Dumping debug info for " + description + ": " + dumpFile.getPath());
+ try (FileOutputStream out = new FileOutputStream(dumpFile)) {
+ for (String cmd : new String[] {
+ "dumpsys netpolicy",
+ "dumpsys network_management",
+ "dumpsys usagestats " + TEST_PKG + " " + TEST_APP2_PKG,
+ "dumpsys usagestats appstandby",
+ }) {
+ dumpCommandOutput(out, cmd);
+ }
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Error opening file: " + dumpFile, e);
+ } catch (IOException e) {
+ Log.e(TAG, "Error closing file: " + dumpFile, e);
+ }
+ }
+
+ void dumpCommandOutput(FileOutputStream out, String cmd) {
+ final ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation().executeShellCommand(cmd);
+ try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
+ out.write(("Output of '" + cmd + "':\n").getBytes(StandardCharsets.UTF_8));
+ FileUtils.copy(in, out);
+ out.write("\n\n=================================================================\n\n"
+ .getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ Log.e(TAG, "Error dumping '" + cmd + "'", e);
+ }
+ }
+
+ void prepareDumpRootDir() {
+ if (!mDumpDir.exists() && !mDumpDir.mkdir()) {
+ Log.e(TAG, "Error creating " + mDumpDir);
+ }
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java
new file mode 100644
index 0000000..8fadf9e
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.resetMeteredNetwork;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setupMeteredNetwork;
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+import android.util.ArraySet;
+import android.util.Pair;
+
+import com.android.compatibility.common.util.BeforeAfterRule;
+
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public class MeterednessConfigurationRule extends BeforeAfterRule {
+ private Pair<String, Boolean> mSsidAndInitialMeteredness;
+
+ @Override
+ public void onBefore(Statement base, Description description) throws Throwable {
+ final ArraySet<Property> requiredProperties
+ = RequiredPropertiesRule.getRequiredProperties();
+ if (requiredProperties.contains(METERED_NETWORK)) {
+ configureNetworkMeteredness(true);
+ } else if (requiredProperties.contains(NON_METERED_NETWORK)) {
+ configureNetworkMeteredness(false);
+ }
+ }
+
+ @Override
+ public void onAfter(Statement base, Description description) throws Throwable {
+ resetNetworkMeteredness();
+ }
+
+ public void configureNetworkMeteredness(boolean metered) throws Exception {
+ mSsidAndInitialMeteredness = setupMeteredNetwork(metered);
+ }
+
+ public void resetNetworkMeteredness() throws Exception {
+ if (mSsidAndInitialMeteredness != null) {
+ resetMeteredNetwork(mSsidAndInitialMeteredness.first,
+ mSsidAndInitialMeteredness.second);
+ }
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MixedModesTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MixedModesTest.java
new file mode 100644
index 0000000..c9edda6
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MixedModesTest.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
+import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DOZE_MODE;
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test cases for the more complex scenarios where multiple restrictions (like Battery Saver Mode
+ * and Data Saver Mode) are applied simultaneously.
+ * <p>
+ * <strong>NOTE: </strong>it might sound like the test methods on this class are testing too much,
+ * which would make it harder to diagnose individual failures, but the assumption is that such
+ * failure most likely will happen when the restriction is tested individually as well.
+ */
+public class MixedModesTest extends AbstractRestrictBackgroundNetworkTestCase {
+ private static final String TAG = "MixedModesTest";
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ // Set initial state.
+ removeRestrictBackgroundWhitelist(mUid);
+ removeRestrictBackgroundBlacklist(mUid);
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+
+ registerBroadcastReceiver();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ try {
+ setRestrictBackground(false);
+ } finally {
+ setBatterySaverMode(false);
+ }
+ }
+
+ /**
+ * Tests all DS ON and BS ON scenarios from network-policy-restrictions.md on metered networks.
+ */
+ @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK})
+ @Test
+ public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
+ final MeterednessConfigurationRule meterednessConfiguration
+ = new MeterednessConfigurationRule();
+ meterednessConfiguration.configureNetworkMeteredness(true);
+ try {
+ setRestrictBackground(true);
+ setBatterySaverMode(true);
+
+ Log.v(TAG, "Not whitelisted for any.");
+ assertBackgroundNetworkAccess(false);
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(false);
+
+ Log.v(TAG, "Whitelisted for Data Saver but not for Battery Saver.");
+ addRestrictBackgroundWhitelist(mUid);
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(false);
+ removeRestrictBackgroundWhitelist(mUid);
+
+ Log.v(TAG, "Whitelisted for Battery Saver but not for Data Saver.");
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ removeRestrictBackgroundWhitelist(mUid);
+ assertBackgroundNetworkAccess(false);
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(false);
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+
+ Log.v(TAG, "Whitelisted for both.");
+ addRestrictBackgroundWhitelist(mUid);
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(true);
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(true);
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+ removeRestrictBackgroundWhitelist(mUid);
+
+ Log.v(TAG, "Blacklisted for Data Saver, not whitelisted for Battery Saver.");
+ addRestrictBackgroundBlacklist(mUid);
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(false);
+ removeRestrictBackgroundBlacklist(mUid);
+
+ Log.v(TAG, "Blacklisted for Data Saver, whitelisted for Battery Saver.");
+ addRestrictBackgroundBlacklist(mUid);
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(false);
+ removeRestrictBackgroundBlacklist(mUid);
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ } finally {
+ meterednessConfiguration.resetNetworkMeteredness();
+ }
+ }
+
+ /**
+ * Tests all DS ON and BS ON scenarios from network-policy-restrictions.md on non-metered
+ * networks.
+ */
+ @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, NON_METERED_NETWORK})
+ @Test
+ public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
+ final MeterednessConfigurationRule meterednessConfiguration
+ = new MeterednessConfigurationRule();
+ meterednessConfiguration.configureNetworkMeteredness(false);
+ try {
+ setRestrictBackground(true);
+ setBatterySaverMode(true);
+
+ Log.v(TAG, "Not whitelisted for any.");
+ assertBackgroundNetworkAccess(false);
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(false);
+
+ Log.v(TAG, "Whitelisted for Data Saver but not for Battery Saver.");
+ addRestrictBackgroundWhitelist(mUid);
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(false);
+ removeRestrictBackgroundWhitelist(mUid);
+
+ Log.v(TAG, "Whitelisted for Battery Saver but not for Data Saver.");
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ removeRestrictBackgroundWhitelist(mUid);
+ assertBackgroundNetworkAccess(true);
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(true);
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+
+ Log.v(TAG, "Whitelisted for both.");
+ addRestrictBackgroundWhitelist(mUid);
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(true);
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(true);
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+ removeRestrictBackgroundWhitelist(mUid);
+
+ Log.v(TAG, "Blacklisted for Data Saver, not whitelisted for Battery Saver.");
+ addRestrictBackgroundBlacklist(mUid);
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(false);
+ removeRestrictBackgroundBlacklist(mUid);
+
+ Log.v(TAG, "Blacklisted for Data Saver, whitelisted for Battery Saver.");
+ addRestrictBackgroundBlacklist(mUid);
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(true);
+ assertsForegroundAlwaysHasNetworkAccess();
+ assertBackgroundNetworkAccess(true);
+ removeRestrictBackgroundBlacklist(mUid);
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ } finally {
+ meterednessConfiguration.resetNetworkMeteredness();
+ }
+ }
+
+ /**
+ * Tests that powersave whitelists works as expected when doze and battery saver modes
+ * are enabled.
+ */
+ @RequiredProperties({DOZE_MODE, BATTERY_SAVER_MODE})
+ @Test
+ public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
+ setBatterySaverMode(true);
+ setDozeMode(true);
+
+ try {
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(true);
+
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+
+ addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+
+ removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+ } finally {
+ setBatterySaverMode(false);
+ setDozeMode(false);
+ }
+ }
+
+ /**
+ * Tests that powersave whitelists works as expected when doze and appIdle modes
+ * are enabled.
+ */
+ @RequiredProperties({DOZE_MODE, APP_STANDBY_MODE})
+ @Test
+ public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
+ setDozeMode(true);
+ setAppIdle(true);
+
+ try {
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(true);
+
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+
+ addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+
+ removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(false);
+ } finally {
+ setAppIdle(false);
+ setDozeMode(false);
+ }
+ }
+
+ @RequiredProperties({APP_STANDBY_MODE, DOZE_MODE})
+ @Test
+ public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
+ setDozeMode(true);
+ setAppIdle(true);
+
+ try {
+ assertBackgroundNetworkAccess(false);
+
+ addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
+ assertBackgroundNetworkAccess(true);
+
+ // Wait until the whitelist duration is expired.
+ SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
+ assertBackgroundNetworkAccess(false);
+ } finally {
+ setAppIdle(false);
+ setDozeMode(false);
+ }
+ }
+
+ @RequiredProperties({APP_STANDBY_MODE, BATTERY_SAVER_MODE})
+ @Test
+ public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
+ setBatterySaverMode(true);
+ setAppIdle(true);
+
+ try {
+ assertBackgroundNetworkAccess(false);
+
+ addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
+ assertBackgroundNetworkAccess(true);
+
+ // Wait until the whitelist duration is expired.
+ SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
+ assertBackgroundNetworkAccess(false);
+ } finally {
+ setAppIdle(false);
+ setBatterySaverMode(false);
+ }
+ }
+
+ /**
+ * Tests that the app idle whitelist works as expected when doze and appIdle mode are enabled.
+ */
+ @RequiredProperties({DOZE_MODE, APP_STANDBY_MODE})
+ @Test
+ public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
+ setDozeMode(true);
+ setAppIdle(true);
+
+ try {
+ assertBackgroundNetworkAccess(false);
+
+ // UID still shouldn't have access because of Doze.
+ addAppIdleWhitelist(mUid);
+ assertBackgroundNetworkAccess(false);
+
+ removeAppIdleWhitelist(mUid);
+ assertBackgroundNetworkAccess(false);
+ } finally {
+ setAppIdle(false);
+ setDozeMode(false);
+ }
+ }
+
+ @RequiredProperties({APP_STANDBY_MODE, DOZE_MODE})
+ @Test
+ public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
+ setDozeMode(true);
+ setAppIdle(true);
+
+ try {
+ assertBackgroundNetworkAccess(false);
+
+ addAppIdleWhitelist(mUid);
+ assertBackgroundNetworkAccess(false);
+
+ addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
+ assertBackgroundNetworkAccess(true);
+
+ // Wait until the whitelist duration is expired.
+ SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
+ assertBackgroundNetworkAccess(false);
+ } finally {
+ setAppIdle(false);
+ setDozeMode(false);
+ removeAppIdleWhitelist(mUid);
+ }
+ }
+
+ @RequiredProperties({APP_STANDBY_MODE, BATTERY_SAVER_MODE})
+ @Test
+ public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
+ setBatterySaverMode(true);
+ setAppIdle(true);
+
+ try {
+ assertBackgroundNetworkAccess(false);
+
+ addAppIdleWhitelist(mUid);
+ assertBackgroundNetworkAccess(false);
+
+ addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
+ assertBackgroundNetworkAccess(true);
+
+ // Wait until the whitelist duration is expired.
+ SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
+ assertBackgroundNetworkAccess(false);
+ } finally {
+ setAppIdle(false);
+ setBatterySaverMode(false);
+ removeAppIdleWhitelist(mUid);
+ }
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java
new file mode 100644
index 0000000..0d0bc58
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 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 com.android.cts.net.hostside;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.WindowManager;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class MyActivity extends Activity {
+ private final LinkedBlockingQueue<Integer> mResult = new LinkedBlockingQueue<>(1);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (mResult.offer(resultCode) == false) {
+ throw new RuntimeException("Queue is full! This should never happen");
+ }
+ }
+
+ public Integer getResult(int timeoutMs) throws InterruptedException {
+ return mResult.poll(timeoutMs, TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java
new file mode 100644
index 0000000..0132536
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.app.RemoteInput;
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+
+/**
+ * NotificationListenerService implementation that executes the notification actions once they're
+ * created.
+ */
+public class MyNotificationListenerService extends NotificationListenerService {
+ private static final String TAG = "MyNotificationListenerService";
+
+ @Override
+ public void onListenerConnected() {
+ Log.d(TAG, "onListenerConnected()");
+ }
+
+ @Override
+ public void onNotificationPosted(StatusBarNotification sbn) {
+ Log.d(TAG, "onNotificationPosted(): " + sbn);
+ if (!sbn.getPackageName().startsWith(getPackageName())) {
+ Log.v(TAG, "ignoring notification from a different package");
+ return;
+ }
+ final PendingIntentSender sender = new PendingIntentSender();
+ final Notification notification = sbn.getNotification();
+ if (notification.contentIntent != null) {
+ sender.send("content", notification.contentIntent);
+ }
+ if (notification.deleteIntent != null) {
+ sender.send("delete", notification.deleteIntent);
+ }
+ if (notification.fullScreenIntent != null) {
+ sender.send("full screen", notification.fullScreenIntent);
+ }
+ if (notification.actions != null) {
+ for (Notification.Action action : notification.actions) {
+ sender.send("action", action.actionIntent);
+ sender.send("action extras", action.getExtras());
+ final RemoteInput[] remoteInputs = action.getRemoteInputs();
+ if (remoteInputs != null && remoteInputs.length > 0) {
+ for (RemoteInput remoteInput : remoteInputs) {
+ sender.send("remote input extras", remoteInput.getExtras());
+ }
+ }
+ }
+ }
+ sender.send("notification extras", notification.extras);
+ }
+
+ static String getId() {
+ return String.format("%s/%s", MyNotificationListenerService.class.getPackage().getName(),
+ MyNotificationListenerService.class.getName());
+ }
+
+ static ComponentName getComponentName() {
+ return new ComponentName(MyNotificationListenerService.class.getPackage().getName(),
+ MyNotificationListenerService.class.getName());
+ }
+
+ private static final class PendingIntentSender {
+ private PendingIntent mSentIntent = null;
+ private String mReason = null;
+
+ private void send(String reason, PendingIntent pendingIntent) {
+ if (pendingIntent == null) {
+ // Could happen on action that only has extras
+ Log.v(TAG, "Not sending null pending intent for " + reason);
+ return;
+ }
+ if (mSentIntent != null || mReason != null) {
+ // Sanity check: make sure test case set up just one pending intent in the
+ // notification, otherwise it could pass because another pending intent caused the
+ // whitelisting.
+ throw new IllegalStateException("Already sent a PendingIntent (" + mSentIntent
+ + ") for reason '" + mReason + "' when requested another for '" + reason
+ + "' (" + pendingIntent + ")");
+ }
+ Log.i(TAG, "Sending pending intent for " + reason + ":" + pendingIntent);
+ try {
+ pendingIntent.send();
+ mSentIntent = pendingIntent;
+ mReason = reason;
+ } catch (CanceledException e) {
+ Log.w(TAG, "Pending intent " + pendingIntent + " canceled");
+ }
+ }
+
+ private void send(String reason, Bundle extras) {
+ if (extras != null) {
+ for (String key : extras.keySet()) {
+ Object value = extras.get(key);
+ if (value instanceof PendingIntent) {
+ send(reason + " with key '" + key + "'", (PendingIntent) value);
+ }
+ }
+ }
+ }
+
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
new file mode 100644
index 0000000..6546e26
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.ConditionVariable;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import com.android.cts.net.hostside.IMyService;
+
+public class MyServiceClient {
+ private static final int TIMEOUT_MS = 5000;
+ private static final String PACKAGE = MyServiceClient.class.getPackage().getName();
+ private static final String APP2_PACKAGE = PACKAGE + ".app2";
+ private static final String SERVICE_NAME = APP2_PACKAGE + ".MyService";
+
+ private Context mContext;
+ private ServiceConnection mServiceConnection;
+ private IMyService mService;
+
+ public MyServiceClient(Context context) {
+ mContext = context;
+ }
+
+ public void bind() {
+ if (mService != null) {
+ throw new IllegalStateException("Already bound");
+ }
+
+ final ConditionVariable cv = new ConditionVariable();
+ mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mService = IMyService.Stub.asInterface(service);
+ cv.open();
+ }
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mService = null;
+ }
+ };
+
+ final Intent intent = new Intent();
+ intent.setComponent(new ComponentName(APP2_PACKAGE, SERVICE_NAME));
+ // Needs to use BIND_NOT_FOREGROUND so app2 does not run in
+ // the same process state as app
+ mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE
+ | Context.BIND_NOT_FOREGROUND);
+ cv.block(TIMEOUT_MS);
+ if (mService == null) {
+ throw new IllegalStateException(
+ "Could not bind to MyService service after " + TIMEOUT_MS + "ms");
+ }
+ }
+
+ public void unbind() {
+ if (mService != null) {
+ mContext.unbindService(mServiceConnection);
+ }
+ }
+
+ public void registerBroadcastReceiver() throws RemoteException {
+ mService.registerBroadcastReceiver();
+ }
+
+ public int getCounters(String receiverName, String action) throws RemoteException {
+ return mService.getCounters(receiverName, action);
+ }
+
+ public String checkNetworkStatus() throws RemoteException {
+ return mService.checkNetworkStatus();
+ }
+
+ public String getRestrictBackgroundStatus() throws RemoteException {
+ return mService.getRestrictBackgroundStatus();
+ }
+
+ public void sendNotification(int notificationId, String notificationType) throws RemoteException {
+ mService.sendNotification(notificationId, notificationType);
+ }
+
+ public void registerNetworkCallback(INetworkCallback cb) throws RemoteException {
+ mService.registerNetworkCallback(cb);
+ }
+
+ public void unregisterNetworkCallback() throws RemoteException {
+ mService.unregisterNetworkCallback();
+ }
+}
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
new file mode 100644
index 0000000..7d3d4fc
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2014 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 com.android.cts.net.hostside;
+
+import android.content.Intent;
+import android.net.Network;
+import android.net.ProxyInfo;
+import android.net.VpnService;
+import android.os.ParcelFileDescriptor;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+
+public class MyVpnService extends VpnService {
+
+ private static String TAG = "MyVpnService";
+ private static int MTU = 1799;
+
+ public static final String ACTION_ESTABLISHED = "com.android.cts.net.hostside.ESTABNLISHED";
+ public static final String EXTRA_ALWAYS_ON = "is-always-on";
+ public static final String EXTRA_LOCKDOWN_ENABLED = "is-lockdown-enabled";
+
+ private ParcelFileDescriptor mFd = null;
+ private PacketReflector mPacketReflector = null;
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ String packageName = getPackageName();
+ String cmd = intent.getStringExtra(packageName + ".cmd");
+ if ("disconnect".equals(cmd)) {
+ stop();
+ } else if ("connect".equals(cmd)) {
+ start(packageName, intent);
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ private void start(String packageName, Intent intent) {
+ Builder builder = new Builder();
+
+ 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;
+ }
+ }
+ }
+
+ 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("/");
+ try {
+ InetAddress address = InetAddress.getByName(prefixAndMask[0]);
+ int prefixLength = Integer.parseInt(prefixAndMask[1]);
+ builder.addRoute(address, prefixLength);
+ } catch (UnknownHostException|NumberFormatException|
+ ArrayIndexOutOfBoundsException e) {
+ continue;
+ }
+ }
+ }
+
+ String allowed = intent.getStringExtra(packageName + ".allowedapplications");
+ if (allowed != null) {
+ String[] packageArray = allowed.split(",");
+ for (int i = 0; i < packageArray.length; i++) {
+ String allowedPackage = packageArray[i];
+ if (!TextUtils.isEmpty(allowedPackage)) {
+ try {
+ builder.addAllowedApplication(allowedPackage);
+ } catch(NameNotFoundException e) {
+ continue;
+ }
+ }
+ }
+ }
+
+ String disallowed = intent.getStringExtra(packageName + ".disallowedapplications");
+ if (disallowed != null) {
+ String[] packageArray = disallowed.split(",");
+ for (int i = 0; i < packageArray.length; i++) {
+ String disallowedPackage = packageArray[i];
+ if (!TextUtils.isEmpty(disallowedPackage)) {
+ try {
+ builder.addDisallowedApplication(disallowedPackage);
+ } catch(NameNotFoundException e) {
+ continue;
+ }
+ }
+ }
+ }
+
+ ArrayList<Network> underlyingNetworks =
+ intent.getParcelableArrayListExtra(packageName + ".underlyingNetworks");
+ if (underlyingNetworks == null) {
+ // VPN tracks default network
+ builder.setUnderlyingNetworks(null);
+ } else {
+ builder.setUnderlyingNetworks(underlyingNetworks.toArray(new Network[0]));
+ }
+
+ boolean isAlwaysMetered = intent.getBooleanExtra(packageName + ".isAlwaysMetered", false);
+ builder.setMetered(isAlwaysMetered);
+
+ ProxyInfo vpnProxy = intent.getParcelableExtra(packageName + ".httpProxy");
+ builder.setHttpProxy(vpnProxy);
+ builder.setMtu(MTU);
+ builder.setBlocking(true);
+ builder.setSession("MyVpnService");
+
+ Log.i(TAG, "Establishing VPN,"
+ + " addresses=" + addresses
+ + " routes=" + routes
+ + " allowedApplications=" + allowed
+ + " disallowedApplications=" + disallowed);
+
+ mFd = builder.establish();
+ Log.i(TAG, "Established, fd=" + (mFd == null ? "null" : mFd.getFd()));
+
+ broadcastEstablished();
+
+ mPacketReflector = new PacketReflector(mFd.getFileDescriptor(), MTU);
+ mPacketReflector.start();
+ }
+
+ private void broadcastEstablished() {
+ final Intent bcIntent = new Intent(ACTION_ESTABLISHED);
+ bcIntent.putExtra(EXTRA_ALWAYS_ON, isAlwaysOn());
+ bcIntent.putExtra(EXTRA_LOCKDOWN_ENABLED, isLockdownEnabled());
+ sendBroadcast(bcIntent);
+ }
+
+ private void stop() {
+ if (mPacketReflector != null) {
+ mPacketReflector.interrupt();
+ mPacketReflector = null;
+ }
+ try {
+ if (mFd != null) {
+ Log.i(TAG, "Closing filedescriptor");
+ mFd.close();
+ }
+ } catch(IOException e) {
+ } finally {
+ mFd = null;
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ stop();
+ super.onDestroy();
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
new file mode 100644
index 0000000..2ac29e7
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.net.hostside;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isActiveNetworkMetered;
+import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.Objects;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCase {
+ private Network mNetwork;
+ private final TestNetworkCallback mTestNetworkCallback = new TestNetworkCallback();
+ @Rule
+ public final MeterednessConfigurationRule mMeterednessConfiguration
+ = new MeterednessConfigurationRule();
+
+ enum CallbackState {
+ NONE,
+ AVAILABLE,
+ LOST,
+ BLOCKED_STATUS,
+ CAPABILITIES
+ }
+
+ private static class CallbackInfo {
+ public final CallbackState state;
+ public final Network network;
+ public final Object arg;
+
+ CallbackInfo(CallbackState s, Network n, Object o) {
+ state = s; network = n; arg = o;
+ }
+
+ public String toString() {
+ return String.format("%s (%s) (%s)", state, network, arg);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof CallbackInfo)) return false;
+ // Ignore timeMs, since it's unpredictable.
+ final CallbackInfo other = (CallbackInfo) o;
+ return (state == other.state) && Objects.equals(network, other.network)
+ && Objects.equals(arg, other.arg);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(state, network, arg);
+ }
+ }
+
+ private class TestNetworkCallback extends INetworkCallback.Stub {
+ private static final int TEST_CONNECT_TIMEOUT_MS = 30_000;
+ private static final int TEST_CALLBACK_TIMEOUT_MS = 5_000;
+
+ private final LinkedBlockingQueue<CallbackInfo> mCallbacks = new LinkedBlockingQueue<>();
+
+ protected void setLastCallback(CallbackState state, Network network, Object o) {
+ mCallbacks.offer(new CallbackInfo(state, network, o));
+ }
+
+ CallbackInfo nextCallback(int timeoutMs) {
+ CallbackInfo cb = null;
+ try {
+ cb = mCallbacks.poll(timeoutMs, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ }
+ if (cb == null) {
+ fail("Did not receive callback after " + timeoutMs + "ms");
+ }
+ return cb;
+ }
+
+ CallbackInfo expectCallback(CallbackState state, Network expectedNetwork, Object o) {
+ final CallbackInfo expected = new CallbackInfo(state, expectedNetwork, o);
+ final CallbackInfo actual = nextCallback(TEST_CALLBACK_TIMEOUT_MS);
+ assertEquals("Unexpected callback:", expected, actual);
+ return actual;
+ }
+
+ @Override
+ public void onAvailable(Network network) {
+ setLastCallback(CallbackState.AVAILABLE, network, null);
+ }
+
+ @Override
+ public void onLost(Network network) {
+ setLastCallback(CallbackState.LOST, network, null);
+ }
+
+ @Override
+ public void onBlockedStatusChanged(Network network, boolean blocked) {
+ setLastCallback(CallbackState.BLOCKED_STATUS, network, blocked);
+ }
+
+ @Override
+ public void onCapabilitiesChanged(Network network, NetworkCapabilities cap) {
+ setLastCallback(CallbackState.CAPABILITIES, network, cap);
+ }
+
+ public Network expectAvailableCallbackAndGetNetwork() {
+ final CallbackInfo cb = nextCallback(TEST_CONNECT_TIMEOUT_MS);
+ if (cb.state != CallbackState.AVAILABLE) {
+ fail("Network is not available. Instead obtained the following callback :"
+ + cb);
+ }
+ return cb.network;
+ }
+
+ public void expectBlockedStatusCallback(Network expectedNetwork, boolean expectBlocked) {
+ expectCallback(CallbackState.BLOCKED_STATUS, expectedNetwork, expectBlocked);
+ }
+
+ public void expectBlockedStatusCallbackEventually(Network expectedNetwork,
+ boolean expectBlocked) {
+ final long deadline = System.currentTimeMillis() + TEST_CALLBACK_TIMEOUT_MS;
+ do {
+ final CallbackInfo cb = nextCallback((int) (deadline - System.currentTimeMillis()));
+ if (cb.state == CallbackState.BLOCKED_STATUS
+ && cb.network.equals(expectedNetwork)) {
+ assertEquals(expectBlocked, cb.arg);
+ return;
+ }
+ } while (System.currentTimeMillis() <= deadline);
+ fail("Didn't receive onBlockedStatusChanged()");
+ }
+
+ public void expectCapabilitiesCallbackEventually(Network expectedNetwork, boolean hasCap,
+ int cap) {
+ final long deadline = System.currentTimeMillis() + TEST_CALLBACK_TIMEOUT_MS;
+ do {
+ final CallbackInfo cb = nextCallback((int) (deadline - System.currentTimeMillis()));
+ if (cb.state != CallbackState.CAPABILITIES
+ || !expectedNetwork.equals(cb.network)
+ || (hasCap != ((NetworkCapabilities) cb.arg).hasCapability(cap))) {
+ Log.i("NetworkCallbackTest#expectCapabilitiesCallback",
+ "Ignoring non-matching callback : " + cb);
+ continue;
+ }
+ // Found a match, return
+ return;
+ } while (System.currentTimeMillis() <= deadline);
+ fail("Didn't receive the expected callback to onCapabilitiesChanged(). Check the "
+ + "log for a list of received callbacks, if any.");
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ assumeTrue(isActiveNetworkMetered(true) || canChangeActiveNetworkMeteredness());
+
+ registerBroadcastReceiver();
+
+ removeRestrictBackgroundWhitelist(mUid);
+ removeRestrictBackgroundBlacklist(mUid);
+ assertRestrictBackgroundChangedReceived(0);
+
+ // Initial state
+ setBatterySaverMode(false);
+ setRestrictBackground(false);
+
+ // Make wifi a metered network.
+ mMeterednessConfiguration.configureNetworkMeteredness(true);
+
+ // Register callback
+ registerNetworkCallback((INetworkCallback.Stub) mTestNetworkCallback);
+ // Once the wifi is marked as metered, the wifi will reconnect. Wait for onAvailable()
+ // callback to ensure wifi is connected before the test and store the default network.
+ mNetwork = mTestNetworkCallback.expectAvailableCallbackAndGetNetwork();
+ // Check that the network is metered.
+ mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
+ false /* hasCapability */, NET_CAPABILITY_NOT_METERED);
+ mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ setRestrictBackground(false);
+ setBatterySaverMode(false);
+ unregisterNetworkCallback();
+ }
+
+ @RequiredProperties({DATA_SAVER_MODE})
+ @Test
+ public void testOnBlockedStatusChanged_dataSaver() throws Exception {
+ try {
+ // Enable restrict background
+ setRestrictBackground(true);
+ assertBackgroundNetworkAccess(false);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+
+ // Add to whitelist
+ addRestrictBackgroundWhitelist(mUid);
+ assertBackgroundNetworkAccess(true);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+
+ // Remove from whitelist
+ removeRestrictBackgroundWhitelist(mUid);
+ assertBackgroundNetworkAccess(false);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+ } finally {
+ mMeterednessConfiguration.resetNetworkMeteredness();
+ }
+
+ // Set to non-metered network
+ mMeterednessConfiguration.configureNetworkMeteredness(false);
+ mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
+ true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
+ try {
+ assertBackgroundNetworkAccess(true);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+
+ // Disable restrict background, should not trigger callback
+ setRestrictBackground(false);
+ assertBackgroundNetworkAccess(true);
+ } finally {
+ mMeterednessConfiguration.resetNetworkMeteredness();
+ }
+ }
+
+ @RequiredProperties({BATTERY_SAVER_MODE})
+ @Test
+ public void testOnBlockedStatusChanged_powerSaver() throws Exception {
+ try {
+ // Enable Power Saver
+ setBatterySaverMode(true);
+ assertBackgroundNetworkAccess(false);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+
+ // Disable Power Saver
+ setBatterySaverMode(false);
+ assertBackgroundNetworkAccess(true);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+ } finally {
+ mMeterednessConfiguration.resetNetworkMeteredness();
+ }
+
+ // Set to non-metered network
+ mMeterednessConfiguration.configureNetworkMeteredness(false);
+ mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
+ true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
+ try {
+ // Enable Power Saver
+ setBatterySaverMode(true);
+ assertBackgroundNetworkAccess(false);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+
+ // Disable Power Saver
+ setBatterySaverMode(false);
+ assertBackgroundNetworkAccess(true);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+ } finally {
+ mMeterednessConfiguration.resetNetworkMeteredness();
+ }
+ }
+
+ // TODO: 1. test against VPN lockdown.
+ // 2. test against multiple networks.
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestRunner.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestRunner.java
new file mode 100644
index 0000000..f340907
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestRunner.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 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 com.android.cts.net.hostside;
+
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import org.junit.rules.RunRules;
+import org.junit.rules.TestRule;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+
+import java.util.List;
+
+/**
+ * Custom runner to allow dumping logs after a test failure before the @After methods get to run.
+ */
+public class NetworkPolicyTestRunner extends AndroidJUnit4ClassRunner {
+ private TestRule mDumpOnFailureRule = new DumpOnFailureRule();
+
+ public NetworkPolicyTestRunner(Class<?> klass) throws InitializationError {
+ super(klass);
+ }
+
+ @Override
+ public Statement methodInvoker(FrameworkMethod method, Object test) {
+ return new RunRules(super.methodInvoker(method, test), List.of(mDumpOnFailureRule),
+ describeChild(method));
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
new file mode 100644
index 0000000..3807d79
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.net.hostside;
+
+import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
+import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
+import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.ActivityManager;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.location.LocationManager;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.wifi.WifiManager;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.compatibility.common.util.AppStandbyUtils;
+import com.android.compatibility.common.util.BatteryUtils;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+public class NetworkPolicyTestUtils {
+
+ private static final int TIMEOUT_CHANGE_METEREDNESS_MS = 5000;
+
+ private static ConnectivityManager mCm;
+ private static WifiManager mWm;
+
+ private static Boolean mBatterySaverSupported;
+ private static Boolean mDataSaverSupported;
+ private static Boolean mDozeModeSupported;
+ private static Boolean mAppStandbySupported;
+
+ private NetworkPolicyTestUtils() {}
+
+ public static boolean isBatterySaverSupported() {
+ if (mBatterySaverSupported == null) {
+ mBatterySaverSupported = BatteryUtils.isBatterySaverSupported();
+ }
+ return mBatterySaverSupported;
+ }
+
+ /**
+ * As per CDD requirements, if the device doesn't support data saver mode then
+ * ConnectivityManager.getRestrictBackgroundStatus() will always return
+ * RESTRICT_BACKGROUND_STATUS_DISABLED. So, enable the data saver mode and check if
+ * ConnectivityManager.getRestrictBackgroundStatus() for an app in background returns
+ * RESTRICT_BACKGROUND_STATUS_DISABLED or not.
+ */
+ public static boolean isDataSaverSupported() {
+ if (mDataSaverSupported == null) {
+ assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED);
+ try {
+ setRestrictBackground(true);
+ mDataSaverSupported = !isMyRestrictBackgroundStatus(
+ RESTRICT_BACKGROUND_STATUS_DISABLED);
+ } finally {
+ setRestrictBackground(false);
+ }
+ }
+ return mDataSaverSupported;
+ }
+
+ public static boolean isDozeModeSupported() {
+ if (mDozeModeSupported == null) {
+ final String result = executeShellCommand("cmd deviceidle enabled deep");
+ mDozeModeSupported = result.equals("1");
+ }
+ return mDozeModeSupported;
+ }
+
+ public static boolean isAppStandbySupported() {
+ if (mAppStandbySupported == null) {
+ mAppStandbySupported = AppStandbyUtils.isAppStandbyEnabled();
+ }
+ return mAppStandbySupported;
+ }
+
+ public static boolean isLowRamDevice() {
+ final ActivityManager am = (ActivityManager) getContext().getSystemService(
+ Context.ACTIVITY_SERVICE);
+ return am.isLowRamDevice();
+ }
+
+ public static boolean isLocationEnabled() {
+ final LocationManager lm = (LocationManager) getContext().getSystemService(
+ Context.LOCATION_SERVICE);
+ return lm.isLocationEnabled();
+ }
+
+ public static void setLocationEnabled(boolean enabled) {
+ final LocationManager lm = (LocationManager) getContext().getSystemService(
+ Context.LOCATION_SERVICE);
+ lm.setLocationEnabledForUser(enabled, Process.myUserHandle());
+ assertEquals("Couldn't change location enabled state", lm.isLocationEnabled(), enabled);
+ Log.d(TAG, "Changed location enabled state to " + enabled);
+ }
+
+ public static boolean isActiveNetworkMetered(boolean metered) {
+ return getConnectivityManager().isActiveNetworkMetered() == metered;
+ }
+
+ public static boolean canChangeActiveNetworkMeteredness() {
+ final Network activeNetwork = getConnectivityManager().getActiveNetwork();
+ final NetworkCapabilities networkCapabilities
+ = getConnectivityManager().getNetworkCapabilities(activeNetwork);
+ return networkCapabilities.hasTransport(TRANSPORT_WIFI);
+ }
+
+ public static Pair<String, Boolean> setupMeteredNetwork(boolean metered) throws Exception {
+ if (isActiveNetworkMetered(metered)) {
+ return null;
+ }
+ final boolean isLocationEnabled = isLocationEnabled();
+ try {
+ if (!isLocationEnabled) {
+ setLocationEnabled(true);
+ }
+ final String ssid = unquoteSSID(getWifiManager().getConnectionInfo().getSSID());
+ assertNotEquals(WifiManager.UNKNOWN_SSID, ssid);
+ setWifiMeteredStatus(ssid, metered);
+ return Pair.create(ssid, !metered);
+ } finally {
+ // Reset the location enabled state
+ if (!isLocationEnabled) {
+ setLocationEnabled(false);
+ }
+ }
+ }
+
+ public static void resetMeteredNetwork(String ssid, boolean metered) throws Exception {
+ setWifiMeteredStatus(ssid, metered);
+ }
+
+ public static void setWifiMeteredStatus(String ssid, boolean metered) throws Exception {
+ assertFalse("SSID should not be empty", TextUtils.isEmpty(ssid));
+ final String cmd = "cmd netpolicy set metered-network " + ssid + " " + metered;
+ executeShellCommand(cmd);
+ assertWifiMeteredStatus(ssid, metered);
+ assertActiveNetworkMetered(metered);
+ }
+
+ public static void assertWifiMeteredStatus(String ssid, boolean expectedMeteredStatus) {
+ final String result = executeShellCommand("cmd netpolicy list wifi-networks");
+ final String expectedLine = ssid + ";" + expectedMeteredStatus;
+ assertTrue("Expected line: " + expectedLine + "; Actual result: " + result,
+ result.contains(expectedLine));
+ }
+
+ // Copied from cts/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
+ public static void assertActiveNetworkMetered(boolean expectedMeteredStatus) throws Exception {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final NetworkCallback networkCallback = new NetworkCallback() {
+ @Override
+ public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
+ final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED);
+ if (metered == expectedMeteredStatus) {
+ latch.countDown();
+ }
+ }
+ };
+ // Registering a callback here guarantees onCapabilitiesChanged is called immediately
+ // with the current setting. Therefore, if the setting has already been changed,
+ // this method will return right away, and if not it will wait for the setting to change.
+ getConnectivityManager().registerDefaultNetworkCallback(networkCallback);
+ if (!latch.await(TIMEOUT_CHANGE_METEREDNESS_MS, TimeUnit.MILLISECONDS)) {
+ fail("Timed out waiting for active network metered status to change to "
+ + expectedMeteredStatus + " ; network = "
+ + getConnectivityManager().getActiveNetwork());
+ }
+ getConnectivityManager().unregisterNetworkCallback(networkCallback);
+ }
+
+ public static void setRestrictBackground(boolean enabled) {
+ executeShellCommand("cmd netpolicy set restrict-background " + enabled);
+ final String output = executeShellCommand("cmd netpolicy get restrict-background");
+ final String expectedSuffix = enabled ? "enabled" : "disabled";
+ assertTrue("output '" + output + "' should end with '" + expectedSuffix + "'",
+ output.endsWith(expectedSuffix));
+ }
+
+ public static boolean isMyRestrictBackgroundStatus(int expectedStatus) {
+ final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus();
+ if (expectedStatus != actualStatus) {
+ Log.d(TAG, "MyRestrictBackgroundStatus: "
+ + "Expected: " + restrictBackgroundValueToString(expectedStatus)
+ + "; Actual: " + restrictBackgroundValueToString(actualStatus));
+ return false;
+ }
+ return true;
+ }
+
+ // Copied from cts/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
+ private static String unquoteSSID(String ssid) {
+ // SSID is returned surrounded by quotes if it can be decoded as UTF-8.
+ // Otherwise it's guaranteed not to start with a quote.
+ if (ssid.charAt(0) == '"') {
+ return ssid.substring(1, ssid.length() - 1);
+ } else {
+ return ssid;
+ }
+ }
+
+ public static String restrictBackgroundValueToString(int status) {
+ switch (status) {
+ case RESTRICT_BACKGROUND_STATUS_DISABLED:
+ return "DISABLED";
+ case RESTRICT_BACKGROUND_STATUS_WHITELISTED:
+ return "WHITELISTED";
+ case RESTRICT_BACKGROUND_STATUS_ENABLED:
+ return "ENABLED";
+ default:
+ return "UNKNOWN_STATUS_" + status;
+ }
+ }
+
+ public static String executeShellCommand(String command) {
+ final String result = runShellCommand(command).trim();
+ Log.d(TAG, "Output of '" + command + "': '" + result + "'");
+ return result;
+ }
+
+ public static void assertMyRestrictBackgroundStatus(int expectedStatus) {
+ final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus();
+ assertEquals(restrictBackgroundValueToString(expectedStatus),
+ restrictBackgroundValueToString(actualStatus));
+ }
+
+ public static ConnectivityManager getConnectivityManager() {
+ if (mCm == null) {
+ mCm = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+ return mCm;
+ }
+
+ public static WifiManager getWifiManager() {
+ if (mWm == null) {
+ mWm = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
+ }
+ return mWm;
+ }
+
+ public static Context getContext() {
+ return getInstrumentation().getContext();
+ }
+
+ public static Instrumentation getInstrumentation() {
+ return InstrumentationRegistry.getInstrumentation();
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/PacketReflector.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/PacketReflector.java
new file mode 100644
index 0000000..124c2c3
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/PacketReflector.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2014 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 com.android.cts.net.hostside;
+
+import static android.system.OsConstants.ICMP6_ECHO_REPLY;
+import static android.system.OsConstants.ICMP6_ECHO_REQUEST;
+import static android.system.OsConstants.ICMP_ECHO;
+import static android.system.OsConstants.ICMP_ECHOREPLY;
+
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+public class PacketReflector extends Thread {
+
+ private static int IPV4_HEADER_LENGTH = 20;
+ private static int IPV6_HEADER_LENGTH = 40;
+
+ private static int IPV4_ADDR_OFFSET = 12;
+ private static int IPV6_ADDR_OFFSET = 8;
+ private static int IPV4_ADDR_LENGTH = 4;
+ private static int IPV6_ADDR_LENGTH = 16;
+
+ private static int IPV4_PROTO_OFFSET = 9;
+ private static int IPV6_PROTO_OFFSET = 6;
+
+ private static final byte IPPROTO_ICMP = 1;
+ private static final byte IPPROTO_TCP = 6;
+ private static final byte IPPROTO_UDP = 17;
+ private static final byte IPPROTO_ICMPV6 = 58;
+
+ private static int ICMP_HEADER_LENGTH = 8;
+ private static int TCP_HEADER_LENGTH = 20;
+ private static int UDP_HEADER_LENGTH = 8;
+
+ private static final byte ICMP_ECHO = 8;
+ private static final byte ICMP_ECHOREPLY = 0;
+
+ private static String TAG = "PacketReflector";
+
+ private FileDescriptor mFd;
+ private byte[] mBuf;
+
+ public PacketReflector(FileDescriptor fd, int mtu) {
+ super("PacketReflector");
+ mFd = fd;
+ mBuf = new byte[mtu];
+ }
+
+ private static void swapBytes(byte[] buf, int pos1, int pos2, int len) {
+ for (int i = 0; i < len; i++) {
+ byte b = buf[pos1 + i];
+ buf[pos1 + i] = buf[pos2 + i];
+ buf[pos2 + i] = b;
+ }
+ }
+
+ private static void swapAddresses(byte[] buf, int version) {
+ int addrPos, addrLen;
+ switch(version) {
+ case 4:
+ addrPos = IPV4_ADDR_OFFSET;
+ addrLen = IPV4_ADDR_LENGTH;
+ break;
+ case 6:
+ addrPos = IPV6_ADDR_OFFSET;
+ addrLen = IPV6_ADDR_LENGTH;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ swapBytes(buf, addrPos, addrPos + addrLen, addrLen);
+ }
+
+ // Reflect TCP packets: swap the source and destination addresses, but don't change the ports.
+ // This is used by the test to "connect to itself" through the VPN.
+ private void processTcpPacket(byte[] buf, int version, int len, int hdrLen) {
+ if (len < hdrLen + TCP_HEADER_LENGTH) {
+ return;
+ }
+
+ // Swap src and dst IP addresses.
+ swapAddresses(buf, version);
+
+ // Send the packet back.
+ writePacket(buf, len);
+ }
+
+ // Echo UDP packets: swap source and destination addresses, and source and destination ports.
+ // This is used by the test to check that the bytes it sends are echoed back.
+ private void processUdpPacket(byte[] buf, int version, int len, int hdrLen) {
+ if (len < hdrLen + UDP_HEADER_LENGTH) {
+ return;
+ }
+
+ // Swap src and dst IP addresses.
+ swapAddresses(buf, version);
+
+ // Swap dst and src ports.
+ int portOffset = hdrLen;
+ swapBytes(buf, portOffset, portOffset + 2, 2);
+
+ // Send the packet back.
+ writePacket(buf, len);
+ }
+
+ private void processIcmpPacket(byte[] buf, int version, int len, int hdrLen) {
+ if (len < hdrLen + ICMP_HEADER_LENGTH) {
+ return;
+ }
+
+ byte type = buf[hdrLen];
+ if (!(version == 4 && type == ICMP_ECHO) &&
+ !(version == 6 && type == (byte) ICMP6_ECHO_REQUEST)) {
+ return;
+ }
+
+ // Save the ping packet we received.
+ byte[] request = buf.clone();
+
+ // Swap src and dst IP addresses, and send the packet back.
+ // This effectively pings the device to see if it replies.
+ swapAddresses(buf, version);
+ writePacket(buf, len);
+
+ // The device should have replied, and buf should now contain a ping response.
+ int received = readPacket(buf);
+ if (received != len) {
+ Log.i(TAG, "Reflecting ping did not result in ping response: " +
+ "read=" + received + " expected=" + len);
+ return;
+ }
+
+ byte replyType = buf[hdrLen];
+ if ((type == ICMP_ECHO && replyType != ICMP_ECHOREPLY)
+ || (type == (byte) ICMP6_ECHO_REQUEST && replyType != (byte) ICMP6_ECHO_REPLY)) {
+ Log.i(TAG, "Received unexpected ICMP reply: original " + type
+ + ", reply " + replyType);
+ return;
+ }
+
+ // Compare the response we got with the original packet.
+ // The only thing that should have changed are addresses, type and checksum.
+ // Overwrite them with the received bytes and see if the packet is otherwise identical.
+ request[hdrLen] = buf[hdrLen]; // Type
+ request[hdrLen + 2] = buf[hdrLen + 2]; // Checksum byte 1.
+ request[hdrLen + 3] = buf[hdrLen + 3]; // Checksum byte 2.
+
+ // Since Linux kernel 4.2, net.ipv6.auto_flowlabels is set by default, and therefore
+ // the request and reply may have different IPv6 flow label: ignore that as well.
+ if (version == 6) {
+ request[1] = (byte)(request[1] & 0xf0 | buf[1] & 0x0f);
+ request[2] = buf[2];
+ request[3] = buf[3];
+ }
+
+ for (int i = 0; i < len; i++) {
+ if (buf[i] != request[i]) {
+ Log.i(TAG, "Received non-matching packet when expecting ping response.");
+ return;
+ }
+ }
+
+ // Now swap the addresses again and reflect the packet. This sends a ping reply.
+ swapAddresses(buf, version);
+ writePacket(buf, len);
+ }
+
+ private void writePacket(byte[] buf, int len) {
+ try {
+ Os.write(mFd, buf, 0, len);
+ } catch (ErrnoException|IOException e) {
+ Log.e(TAG, "Error writing packet: " + e.getMessage());
+ }
+ }
+
+ private int readPacket(byte[] buf) {
+ int len;
+ try {
+ len = Os.read(mFd, buf, 0, buf.length);
+ } catch (ErrnoException|IOException e) {
+ Log.e(TAG, "Error reading packet: " + e.getMessage());
+ len = -1;
+ }
+ return len;
+ }
+
+ // Reads one packet from our mFd, and possibly writes the packet back.
+ private void processPacket() {
+ int len = readPacket(mBuf);
+ if (len < 1) {
+ return;
+ }
+
+ int version = mBuf[0] >> 4;
+ int addrPos, protoPos, hdrLen, addrLen;
+ if (version == 4) {
+ hdrLen = IPV4_HEADER_LENGTH;
+ protoPos = IPV4_PROTO_OFFSET;
+ addrPos = IPV4_ADDR_OFFSET;
+ addrLen = IPV4_ADDR_LENGTH;
+ } else if (version == 6) {
+ hdrLen = IPV6_HEADER_LENGTH;
+ protoPos = IPV6_PROTO_OFFSET;
+ addrPos = IPV6_ADDR_OFFSET;
+ addrLen = IPV6_ADDR_LENGTH;
+ } else {
+ return;
+ }
+
+ if (len < hdrLen) {
+ return;
+ }
+
+ byte proto = mBuf[protoPos];
+ switch (proto) {
+ case IPPROTO_ICMP:
+ case IPPROTO_ICMPV6:
+ processIcmpPacket(mBuf, version, len, hdrLen);
+ break;
+ case IPPROTO_TCP:
+ processTcpPacket(mBuf, version, len, hdrLen);
+ break;
+ case IPPROTO_UDP:
+ processUdpPacket(mBuf, version, len, hdrLen);
+ break;
+ }
+ }
+
+ public void run() {
+ Log.i(TAG, "PacketReflector starting fd=" + mFd + " valid=" + mFd.valid());
+ while (!interrupted() && mFd.valid()) {
+ processPacket();
+ }
+ Log.i(TAG, "PacketReflector exiting fd=" + mFd + " valid=" + mFd.valid());
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/Property.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/Property.java
new file mode 100644
index 0000000..18805f9
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/Property.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isActiveNetworkMetered;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isAppStandbySupported;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isBatterySaverSupported;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDataSaverSupported;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isLowRamDevice;
+
+public enum Property {
+ BATTERY_SAVER_MODE(1 << 0) {
+ public boolean isSupported() { return isBatterySaverSupported(); }
+ },
+
+ DATA_SAVER_MODE(1 << 1) {
+ public boolean isSupported() { return isDataSaverSupported(); }
+ },
+
+ NO_DATA_SAVER_MODE(~DATA_SAVER_MODE.getValue()) {
+ public boolean isSupported() { return !isDataSaverSupported(); }
+ },
+
+ DOZE_MODE(1 << 2) {
+ public boolean isSupported() { return isDozeModeSupported(); }
+ },
+
+ APP_STANDBY_MODE(1 << 3) {
+ public boolean isSupported() { return isAppStandbySupported(); }
+ },
+
+ NOT_LOW_RAM_DEVICE(1 << 4) {
+ public boolean isSupported() { return !isLowRamDevice(); }
+ },
+
+ METERED_NETWORK(1 << 5) {
+ public boolean isSupported() {
+ return isActiveNetworkMetered(true) || canChangeActiveNetworkMeteredness();
+ }
+ },
+
+ NON_METERED_NETWORK(~METERED_NETWORK.getValue()) {
+ public boolean isSupported() {
+ return isActiveNetworkMetered(false) || canChangeActiveNetworkMeteredness();
+ }
+ };
+
+ private int mValue;
+
+ Property(int value) { mValue = value; }
+
+ public int getValue() { return mValue; }
+
+ abstract boolean isSupported();
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java
new file mode 100644
index 0000000..80f99b6
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.ConditionVariable;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.cts.net.hostside.IRemoteSocketFactory;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+public class RemoteSocketFactoryClient {
+ private static final int TIMEOUT_MS = 5000;
+ private static final String PACKAGE = RemoteSocketFactoryClient.class.getPackage().getName();
+ private static final String APP2_PACKAGE = PACKAGE + ".app2";
+ private static final String SERVICE_NAME = APP2_PACKAGE + ".RemoteSocketFactoryService";
+
+ private Context mContext;
+ private ServiceConnection mServiceConnection;
+ private IRemoteSocketFactory mService;
+
+ public RemoteSocketFactoryClient(Context context) {
+ mContext = context;
+ }
+
+ public void bind() {
+ if (mService != null) {
+ throw new IllegalStateException("Already bound");
+ }
+
+ final ConditionVariable cv = new ConditionVariable();
+ mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mService = IRemoteSocketFactory.Stub.asInterface(service);
+ cv.open();
+ }
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mService = null;
+ }
+ };
+
+ final Intent intent = new Intent();
+ intent.setComponent(new ComponentName(APP2_PACKAGE, SERVICE_NAME));
+ mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ cv.block(TIMEOUT_MS);
+ if (mService == null) {
+ throw new IllegalStateException(
+ "Could not bind to RemoteSocketFactory service after " + TIMEOUT_MS + "ms");
+ }
+ }
+
+ public void unbind() {
+ if (mService != null) {
+ mContext.unbindService(mServiceConnection);
+ }
+ }
+
+ public FileDescriptor openSocketFd(String host, int port, int timeoutMs)
+ throws RemoteException, ErrnoException, IOException {
+ // Dup the filedescriptor so ParcelFileDescriptor's finalizer doesn't garbage collect it
+ // and cause our fd to become invalid. http://b/35927643 .
+ ParcelFileDescriptor pfd = mService.openSocketFd(host, port, timeoutMs);
+ FileDescriptor fd = Os.dup(pfd.getFileDescriptor());
+ pfd.close();
+ return fd;
+ }
+
+ public String getPackageName() throws RemoteException {
+ return mService.getPackageName();
+ }
+
+ public int getUid() throws RemoteException {
+ return mService.getUid();
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredProperties.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredProperties.java
new file mode 100644
index 0000000..96838bb
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredProperties.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.net.hostside;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target({METHOD, TYPE})
+@Inherited
+public @interface RequiredProperties {
+ Property[] value();
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredPropertiesRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredPropertiesRule.java
new file mode 100644
index 0000000..01f9f3e
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredPropertiesRule.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
+
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.compatibility.common.util.BeforeAfterRule;
+
+import org.junit.Assume;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+public class RequiredPropertiesRule extends BeforeAfterRule {
+
+ private static ArraySet<Property> mRequiredProperties;
+
+ @Override
+ public void onBefore(Statement base, Description description) {
+ mRequiredProperties = getAllRequiredProperties(description);
+
+ final String testName = description.getClassName() + "#" + description.getMethodName();
+ assertTestIsValid(testName, mRequiredProperties);
+ Log.i(TAG, "Running test " + testName + " with required properties: "
+ + propertiesToString(mRequiredProperties));
+ }
+
+ private ArraySet<Property> getAllRequiredProperties(Description description) {
+ final ArraySet<Property> allRequiredProperties = new ArraySet<>();
+ RequiredProperties requiredProperties = description.getAnnotation(RequiredProperties.class);
+ if (requiredProperties != null) {
+ Collections.addAll(allRequiredProperties, requiredProperties.value());
+ }
+
+ for (Class<?> clazz = description.getTestClass();
+ clazz != null; clazz = clazz.getSuperclass()) {
+ requiredProperties = clazz.getDeclaredAnnotation(RequiredProperties.class);
+ if (requiredProperties == null) {
+ continue;
+ }
+ for (Property requiredProperty : requiredProperties.value()) {
+ for (Property p : Property.values()) {
+ if (p.getValue() == ~requiredProperty.getValue()
+ && allRequiredProperties.contains(p)) {
+ continue;
+ }
+ }
+ allRequiredProperties.add(requiredProperty);
+ }
+ }
+ return allRequiredProperties;
+ }
+
+ private void assertTestIsValid(String testName, ArraySet<Property> requiredProperies) {
+ if (requiredProperies == null) {
+ return;
+ }
+ final ArrayList<Property> unsupportedProperties = new ArrayList<>();
+ for (Property property : requiredProperies) {
+ if (!property.isSupported()) {
+ unsupportedProperties.add(property);
+ }
+ }
+ Assume.assumeTrue("Unsupported properties: "
+ + propertiesToString(unsupportedProperties), unsupportedProperties.isEmpty());
+ }
+
+ public static ArraySet<Property> getRequiredProperties() {
+ return mRequiredProperties;
+ }
+
+ private static String propertiesToString(Iterable<Property> properties) {
+ return "[" + TextUtils.join(",", properties) + "]";
+ }
+}
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
new file mode 100755
index 0000000..81a431c
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -0,0 +1,1165 @@
+/*
+ * Copyright (C) 2014 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 com.android.cts.net.hostside;
+
+import static android.os.Process.INVALID_UID;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.ECONNABORTED;
+import static android.system.OsConstants.IPPROTO_ICMP;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.POLLIN;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import android.annotation.Nullable;
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.Proxy;
+import android.net.ProxyInfo;
+import android.net.Uri;
+import android.net.VpnService;
+import android.net.wifi.WifiManager;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiSelector;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.system.StructPollfd;
+import android.test.InstrumentationTestCase;
+import android.test.MoreAsserts;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for the VpnService API.
+ *
+ * These tests establish a VPN via the VpnService API, and have the service reflect the packets back
+ * to the device without causing any network traffic. This allows testing the local VPN data path
+ * without a network connection or a VPN server.
+ *
+ * Note: in Lollipop, VPN functionality relies on kernel support for UID-based routing. If these
+ * tests fail, it may be due to the lack of kernel support. The necessary patches can be
+ * cherry-picked from the Android common kernel trees:
+ *
+ * android-3.10:
+ * https://android-review.googlesource.com/#/c/99220/
+ * https://android-review.googlesource.com/#/c/100545/
+ *
+ * android-3.4:
+ * https://android-review.googlesource.com/#/c/99225/
+ * https://android-review.googlesource.com/#/c/100557/
+ *
+ * To ensure that the kernel has the required commits, run the kernel unit
+ * tests described at:
+ *
+ * https://source.android.com/devices/tech/config/kernel_network_tests.html
+ *
+ */
+public class VpnTest extends InstrumentationTestCase {
+
+ // These are neither public nor @TestApi.
+ // TODO: add them to @TestApi.
+ private static final String PRIVATE_DNS_MODE_SETTING = "private_dns_mode";
+ private static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = "hostname";
+ private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic";
+ private static final String PRIVATE_DNS_SPECIFIER_SETTING = "private_dns_specifier";
+
+ public static String TAG = "VpnTest";
+ public static int TIMEOUT_MS = 3 * 1000;
+ public static int SOCKET_TIMEOUT_MS = 100;
+ public static String TEST_HOST = "connectivitycheck.gstatic.com";
+
+ private UiDevice mDevice;
+ private MyActivity mActivity;
+ private String mPackageName;
+ private ConnectivityManager mCM;
+ private WifiManager mWifiManager;
+ private RemoteSocketFactoryClient mRemoteSocketFactoryClient;
+
+ Network mNetwork;
+ NetworkCallback mCallback;
+ final Object mLock = new Object();
+ final Object mLockShutdown = new Object();
+
+ private String mOldPrivateDnsMode;
+ private String mOldPrivateDnsSpecifier;
+
+ private boolean supportedHardware() {
+ final PackageManager pm = getInstrumentation().getContext().getPackageManager();
+ return !pm.hasSystemFeature("android.hardware.type.watch");
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mNetwork = null;
+ mCallback = null;
+ storePrivateDnsSetting();
+
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(),
+ MyActivity.class, null);
+ mPackageName = mActivity.getPackageName();
+ mCM = (ConnectivityManager) mActivity.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mWifiManager = (WifiManager) mActivity.getSystemService(Context.WIFI_SERVICE);
+ mRemoteSocketFactoryClient = new RemoteSocketFactoryClient(mActivity);
+ mRemoteSocketFactoryClient.bind();
+ mDevice.waitForIdle();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ restorePrivateDnsSetting();
+ mRemoteSocketFactoryClient.unbind();
+ if (mCallback != null) {
+ mCM.unregisterNetworkCallback(mCallback);
+ }
+ Log.i(TAG, "Stopping VPN");
+ stopVpn();
+ mActivity.finish();
+ super.tearDown();
+ }
+
+ private void prepareVpn() throws Exception {
+ final int REQUEST_ID = 42;
+
+ // Attempt to prepare.
+ Log.i(TAG, "Preparing VPN");
+ Intent intent = VpnService.prepare(mActivity);
+
+ if (intent != null) {
+ // Start the confirmation dialog and click OK.
+ mActivity.startActivityForResult(intent, REQUEST_ID);
+ mDevice.waitForIdle();
+
+ String packageName = intent.getComponent().getPackageName();
+ String resourceIdRegex = "android:id/button1$|button_start_vpn";
+ final UiObject okButton = new UiObject(new UiSelector()
+ .className("android.widget.Button")
+ .packageName(packageName)
+ .resourceIdMatches(resourceIdRegex));
+ if (okButton.waitForExists(TIMEOUT_MS) == false) {
+ mActivity.finishActivity(REQUEST_ID);
+ fail("VpnService.prepare returned an Intent for '" + intent.getComponent() + "' " +
+ "to display the VPN confirmation dialog, but this test could not find the " +
+ "button to allow the VPN application to connect. Please ensure that the " +
+ "component displays a button with a resource ID matching the regexp: '" +
+ resourceIdRegex + "'.");
+ }
+
+ // Click the button and wait for RESULT_OK.
+ okButton.click();
+ try {
+ int result = mActivity.getResult(TIMEOUT_MS);
+ if (result != MyActivity.RESULT_OK) {
+ fail("The VPN confirmation dialog did not return RESULT_OK when clicking on " +
+ "the button matching the regular expression '" + resourceIdRegex +
+ "' of " + intent.getComponent() + "'. Please ensure that clicking on " +
+ "that button allows the VPN application to connect. " +
+ "Return value: " + result);
+ }
+ } catch (InterruptedException e) {
+ fail("VPN confirmation dialog did not return after " + TIMEOUT_MS + "ms");
+ }
+
+ // Now we should be prepared.
+ intent = VpnService.prepare(mActivity);
+ if (intent != null) {
+ fail("VpnService.prepare returned non-null even after the VPN dialog " +
+ intent.getComponent() + "returned RESULT_OK.");
+ }
+ }
+ }
+
+ // 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 {
+ prepareVpn();
+
+ // Register a callback so we will be notified when our VPN comes up.
+ final NetworkRequest request = new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build();
+ mCallback = new NetworkCallback() {
+ public void onAvailable(Network network) {
+ synchronized (mLock) {
+ Log.i(TAG, "Got available callback for network=" + network);
+ mNetwork = network;
+ mLock.notify();
+ }
+ }
+ };
+ mCM.registerNetworkCallback(request, mCallback); // Unregistered in tearDown.
+
+ // Start the service and wait up for TIMEOUT_MS ms for the VPN to come up.
+ Intent intent = new Intent(mActivity, MyVpnService.class)
+ .putExtra(mPackageName + ".cmd", "connect")
+ .putExtra(mPackageName + ".addresses", TextUtils.join(",", addresses))
+ .putExtra(mPackageName + ".routes", TextUtils.join(",", routes))
+ .putExtra(mPackageName + ".allowedapplications", allowedApplications)
+ .putExtra(mPackageName + ".disallowedapplications", disallowedApplications)
+ .putExtra(mPackageName + ".httpProxy", proxyInfo)
+ .putParcelableArrayListExtra(
+ mPackageName + ".underlyingNetworks", underlyingNetworks)
+ .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered);
+
+ mActivity.startService(intent);
+ synchronized (mLock) {
+ if (mNetwork == null) {
+ Log.i(TAG, "bf mLock");
+ mLock.wait(TIMEOUT_MS);
+ Log.i(TAG, "af mLock");
+ }
+ }
+
+ if (mNetwork == null) {
+ fail("VPN did not become available after " + TIMEOUT_MS + "ms");
+ }
+
+ // Unfortunately, when the available callback fires, the VPN UID ranges are not yet
+ // configured. Give the system some time to do so. http://b/18436087 .
+ try { Thread.sleep(3000); } catch(InterruptedException e) {}
+ }
+
+ private void stopVpn() {
+ // Register a callback so we will be notified when our VPN comes up.
+ final NetworkRequest request = new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build();
+ mCallback = new NetworkCallback() {
+ public void onLost(Network network) {
+ synchronized (mLockShutdown) {
+ Log.i(TAG, "Got lost callback for network=" + network
+ + ",mNetwork = " + mNetwork);
+ if( mNetwork == network){
+ mLockShutdown.notify();
+ }
+ }
+ }
+ };
+ mCM.registerNetworkCallback(request, mCallback); // Unregistered in tearDown.
+ // Simply calling mActivity.stopService() won't stop the service, because the system binds
+ // to the service for the purpose of sending it a revoke command if another VPN comes up,
+ // and stopping a bound service has no effect. Instead, "start" the service again with an
+ // Intent that tells it to disconnect.
+ Intent intent = new Intent(mActivity, MyVpnService.class)
+ .putExtra(mPackageName + ".cmd", "disconnect");
+ mActivity.startService(intent);
+ synchronized (mLockShutdown) {
+ try {
+ Log.i(TAG, "bf mLockShutdown");
+ mLockShutdown.wait(TIMEOUT_MS);
+ Log.i(TAG, "af mLockShutdown");
+ } catch(InterruptedException e) {}
+ }
+ }
+
+ private static void closeQuietly(Closeable c) {
+ if (c != null) {
+ try {
+ c.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ private static void checkPing(String to) throws IOException, ErrnoException {
+ InetAddress address = InetAddress.getByName(to);
+ FileDescriptor s;
+ final int LENGTH = 64;
+ byte[] packet = new byte[LENGTH];
+ byte[] header;
+
+ // Construct a ping packet.
+ Random random = new Random();
+ random.nextBytes(packet);
+ if (address instanceof Inet6Address) {
+ s = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
+ header = new byte[] { (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
+ } else {
+ // Note that this doesn't actually work due to http://b/18558481 .
+ s = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
+ header = new byte[] { (byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
+ }
+ System.arraycopy(header, 0, packet, 0, header.length);
+
+ // Send the packet.
+ int port = random.nextInt(65534) + 1;
+ Os.connect(s, address, port);
+ Os.write(s, packet, 0, packet.length);
+
+ // Expect a reply.
+ StructPollfd pollfd = new StructPollfd();
+ pollfd.events = (short) POLLIN; // "error: possible loss of precision"
+ pollfd.fd = s;
+ int ret = Os.poll(new StructPollfd[] { pollfd }, SOCKET_TIMEOUT_MS);
+ assertEquals("Expected reply after sending ping", 1, ret);
+
+ byte[] reply = new byte[LENGTH];
+ int read = Os.read(s, reply, 0, LENGTH);
+ assertEquals(LENGTH, read);
+
+ // Find out what the kernel set the ICMP ID to.
+ InetSocketAddress local = (InetSocketAddress) Os.getsockname(s);
+ port = local.getPort();
+ packet[4] = (byte) ((port >> 8) & 0xff);
+ packet[5] = (byte) (port & 0xff);
+
+ // Check the contents.
+ if (packet[0] == (byte) 0x80) {
+ packet[0] = (byte) 0x81;
+ } else {
+ packet[0] = 0;
+ }
+ // Zero out the checksum in the reply so it matches the uninitialized checksum in packet.
+ reply[2] = reply[3] = 0;
+ MoreAsserts.assertEquals(packet, reply);
+ }
+
+ // Writes data to out and checks that it appears identically on in.
+ private static void writeAndCheckData(
+ OutputStream out, InputStream in, byte[] data) throws IOException {
+ out.write(data, 0, data.length);
+ out.flush();
+
+ byte[] read = new byte[data.length];
+ int bytesRead = 0, totalRead = 0;
+ do {
+ bytesRead = in.read(read, totalRead, read.length - totalRead);
+ totalRead += bytesRead;
+ } while (bytesRead >= 0 && totalRead < data.length);
+ assertEquals(totalRead, data.length);
+ MoreAsserts.assertEquals(data, read);
+ }
+
+ private void checkTcpReflection(String to, String expectedFrom) throws IOException {
+ // Exercise TCP over the VPN by "connecting to ourselves". We open a server socket and a
+ // client socket, and connect the client socket to a remote host, with the port of the
+ // server socket. The PacketReflector reflects the packets, changing the source addresses
+ // but not the ports, so our client socket is connected to our server socket, though both
+ // sockets think their peers are on the "remote" IP address.
+
+ // Open a listening socket.
+ ServerSocket listen = new ServerSocket(0, 10, InetAddress.getByName("::"));
+
+ // Connect the client socket to it.
+ InetAddress toAddr = InetAddress.getByName(to);
+ Socket client = new Socket();
+ try {
+ client.connect(new InetSocketAddress(toAddr, listen.getLocalPort()), SOCKET_TIMEOUT_MS);
+ if (expectedFrom == null) {
+ closeQuietly(listen);
+ closeQuietly(client);
+ fail("Expected connection to fail, but it succeeded.");
+ }
+ } catch (IOException e) {
+ if (expectedFrom != null) {
+ closeQuietly(listen);
+ fail("Expected connection to succeed, but it failed.");
+ } else {
+ // We expected the connection to fail, and it did, so there's nothing more to test.
+ return;
+ }
+ }
+
+ // The connection succeeded, and we expected it to succeed. Send some data; if things are
+ // working, the data will be sent to the VPN, reflected by the PacketReflector, and arrive
+ // at our server socket. For good measure, send some data in the other direction.
+ Socket server = null;
+ try {
+ // Accept the connection on the server side.
+ listen.setSoTimeout(SOCKET_TIMEOUT_MS);
+ server = listen.accept();
+ checkConnectionOwnerUidTcp(client);
+ checkConnectionOwnerUidTcp(server);
+ // Check that the source and peer addresses are as expected.
+ assertEquals(expectedFrom, client.getLocalAddress().getHostAddress());
+ assertEquals(expectedFrom, server.getLocalAddress().getHostAddress());
+ assertEquals(
+ new InetSocketAddress(toAddr, client.getLocalPort()),
+ server.getRemoteSocketAddress());
+ assertEquals(
+ new InetSocketAddress(toAddr, server.getLocalPort()),
+ client.getRemoteSocketAddress());
+
+ // Now write some data.
+ final int LENGTH = 32768;
+ byte[] data = new byte[LENGTH];
+ new Random().nextBytes(data);
+
+ // Make sure our writes don't block or time out, because we're single-threaded and can't
+ // read and write at the same time.
+ server.setReceiveBufferSize(LENGTH * 2);
+ client.setSendBufferSize(LENGTH * 2);
+ client.setSoTimeout(SOCKET_TIMEOUT_MS);
+ server.setSoTimeout(SOCKET_TIMEOUT_MS);
+
+ // Send some data from client to server, then from server to client.
+ writeAndCheckData(client.getOutputStream(), server.getInputStream(), data);
+ writeAndCheckData(server.getOutputStream(), client.getInputStream(), data);
+ } finally {
+ closeQuietly(listen);
+ closeQuietly(client);
+ closeQuietly(server);
+ }
+ }
+
+ private void checkConnectionOwnerUidUdp(DatagramSocket s, boolean expectSuccess) {
+ final int expectedUid = expectSuccess ? Process.myUid() : INVALID_UID;
+ InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort());
+ InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort());
+ int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_UDP, loc, rem);
+ assertEquals(expectedUid, uid);
+ }
+
+ private void checkConnectionOwnerUidTcp(Socket s) {
+ final int expectedUid = Process.myUid();
+ InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort());
+ InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort());
+ int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_TCP, loc, rem);
+ assertEquals(expectedUid, uid);
+ }
+
+ private void checkUdpEcho(String to, String expectedFrom) throws IOException {
+ DatagramSocket s;
+ InetAddress address = InetAddress.getByName(to);
+ if (address instanceof Inet6Address) { // http://b/18094870
+ s = new DatagramSocket(0, InetAddress.getByName("::"));
+ } else {
+ s = new DatagramSocket();
+ }
+ s.setSoTimeout(SOCKET_TIMEOUT_MS);
+
+ Random random = new Random();
+ byte[] data = new byte[random.nextInt(1650)];
+ random.nextBytes(data);
+ DatagramPacket p = new DatagramPacket(data, data.length);
+ s.connect(address, 7);
+
+ if (expectedFrom != null) {
+ assertEquals("Unexpected source address: ",
+ expectedFrom, s.getLocalAddress().getHostAddress());
+ }
+
+ try {
+ if (expectedFrom != null) {
+ s.send(p);
+ checkConnectionOwnerUidUdp(s, true);
+ s.receive(p);
+ MoreAsserts.assertEquals(data, p.getData());
+ } else {
+ try {
+ s.send(p);
+ s.receive(p);
+ fail("Received unexpected reply");
+ } catch (IOException expected) {
+ checkConnectionOwnerUidUdp(s, false);
+ }
+ }
+ } finally {
+ s.close();
+ }
+ }
+
+ 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");
+ }
+
+ 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);
+ }
+
+ private FileDescriptor openSocketFd(String host, int port, int timeoutMs) throws Exception {
+ Socket s = new Socket(host, port);
+ s.setSoTimeout(timeoutMs);
+ // Dup the filedescriptor so ParcelFileDescriptor's finalizer doesn't garbage collect it
+ // and cause our fd to become invalid. http://b/35927643 .
+ FileDescriptor fd = Os.dup(ParcelFileDescriptor.fromSocket(s).getFileDescriptor());
+ s.close();
+ return fd;
+ }
+
+ private FileDescriptor openSocketFdInOtherApp(
+ String host, int port, int timeoutMs) throws Exception {
+ Log.d(TAG, String.format("Creating test socket in UID=%d, my UID=%d",
+ mRemoteSocketFactoryClient.getUid(), Os.getuid()));
+ FileDescriptor fd = mRemoteSocketFactoryClient.openSocketFd(host, port, TIMEOUT_MS);
+ return fd;
+ }
+
+ private void sendRequest(FileDescriptor fd, String host) throws Exception {
+ String request = "GET /generate_204 HTTP/1.1\r\n" +
+ "Host: " + host + "\r\n" +
+ "Connection: keep-alive\r\n\r\n";
+ byte[] requestBytes = request.getBytes(StandardCharsets.UTF_8);
+ int ret = Os.write(fd, requestBytes, 0, requestBytes.length);
+ Log.d(TAG, "Wrote " + ret + "bytes");
+
+ String expected = "HTTP/1.1 204 No Content\r\n";
+ byte[] response = new byte[expected.length()];
+ Os.read(fd, response, 0, response.length);
+
+ String actual = new String(response, StandardCharsets.UTF_8);
+ assertEquals(expected, actual);
+ Log.d(TAG, "Got response: " + actual);
+ }
+
+ private void assertSocketStillOpen(FileDescriptor fd, String host) throws Exception {
+ try {
+ assertTrue(fd.valid());
+ sendRequest(fd, host);
+ assertTrue(fd.valid());
+ } finally {
+ Os.close(fd);
+ }
+ }
+
+ private void assertSocketClosed(FileDescriptor fd, String host) throws Exception {
+ try {
+ assertTrue(fd.valid());
+ sendRequest(fd, host);
+ fail("Socket opened before VPN connects should be closed when VPN connects");
+ } catch (ErrnoException expected) {
+ assertEquals(ECONNABORTED, expected.errno);
+ assertTrue(fd.valid());
+ } finally {
+ Os.close(fd);
+ }
+ }
+
+ private ContentResolver getContentResolver() {
+ return getInstrumentation().getContext().getContentResolver();
+ }
+
+ private boolean isPrivateDnsInStrictMode() {
+ return PRIVATE_DNS_MODE_PROVIDER_HOSTNAME.equals(
+ Settings.Global.getString(getContentResolver(), PRIVATE_DNS_MODE_SETTING));
+ }
+
+ private void storePrivateDnsSetting() {
+ mOldPrivateDnsMode = Settings.Global.getString(getContentResolver(),
+ PRIVATE_DNS_MODE_SETTING);
+ mOldPrivateDnsSpecifier = Settings.Global.getString(getContentResolver(),
+ PRIVATE_DNS_SPECIFIER_SETTING);
+ }
+
+ private void restorePrivateDnsSetting() {
+ Settings.Global.putString(getContentResolver(), PRIVATE_DNS_MODE_SETTING,
+ mOldPrivateDnsMode);
+ Settings.Global.putString(getContentResolver(), PRIVATE_DNS_SPECIFIER_SETTING,
+ mOldPrivateDnsSpecifier);
+ }
+
+ // TODO: replace with CtsNetUtils.awaitPrivateDnsSetting in Q or above.
+ private void expectPrivateDnsHostname(final String hostname) throws Exception {
+ final NetworkRequest request = new NetworkRequest.Builder()
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .build();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final NetworkCallback callback = new NetworkCallback() {
+ @Override
+ public void onLinkPropertiesChanged(Network network, LinkProperties lp) {
+ if (network.equals(mNetwork) &&
+ Objects.equals(lp.getPrivateDnsServerName(), hostname)) {
+ latch.countDown();
+ }
+ }
+ };
+
+ mCM.registerNetworkCallback(request, callback);
+
+ try {
+ assertTrue("Private DNS hostname was not " + hostname + " after " + TIMEOUT_MS + "ms",
+ latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } finally {
+ mCM.unregisterNetworkCallback(callback);
+ }
+ }
+
+ private void setAndVerifyPrivateDns(boolean strictMode) throws Exception {
+ final ContentResolver cr = getInstrumentation().getContext().getContentResolver();
+ String privateDnsHostname;
+
+ if (strictMode) {
+ privateDnsHostname = "vpncts-nx.metric.gstatic.com";
+ Settings.Global.putString(cr, PRIVATE_DNS_SPECIFIER_SETTING, privateDnsHostname);
+ Settings.Global.putString(cr, PRIVATE_DNS_MODE_SETTING,
+ PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
+ } else {
+ Settings.Global.putString(cr, PRIVATE_DNS_MODE_SETTING, PRIVATE_DNS_MODE_OPPORTUNISTIC);
+ privateDnsHostname = null;
+ }
+
+ expectPrivateDnsHostname(privateDnsHostname);
+
+ String randomName = "vpncts-" + new Random().nextInt(1000000000) + "-ds.metric.gstatic.com";
+ if (strictMode) {
+ // Strict mode private DNS is enabled. DNS lookups should fail, because the private DNS
+ // server name is invalid.
+ try {
+ InetAddress.getByName(randomName);
+ fail("VPN DNS lookup should fail with private DNS enabled");
+ } catch (UnknownHostException expected) {
+ }
+ } else {
+ // Strict mode private DNS is disabled. DNS lookup should succeed, because the VPN
+ // provides no DNS servers, and thus DNS falls through to the default network.
+ assertNotNull("VPN DNS lookup should succeed with private DNS disabled",
+ InetAddress.getByName(randomName));
+ }
+ }
+
+ // Tests that strict mode private DNS is used on VPNs.
+ private void checkStrictModePrivateDns() throws Exception {
+ final boolean initialMode = isPrivateDnsInStrictMode();
+ setAndVerifyPrivateDns(!initialMode);
+ setAndVerifyPrivateDns(initialMode);
+ }
+
+ public void testDefault() throws Exception {
+ if (!supportedHardware()) return;
+ // If adb TCP port opened, this test may running by adb over network.
+ // All of socket would be destroyed in this test. So this test don't
+ // support adb over network, see b/119382723.
+ if (SystemProperties.getInt("persist.adb.tcp.port", -1) > -1
+ || SystemProperties.getInt("service.adb.tcp.port", -1) > -1) {
+ Log.i(TAG, "adb is running over the network, so skip this test");
+ return;
+ }
+
+ final BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver(
+ getInstrumentation().getTargetContext(), MyVpnService.ACTION_ESTABLISHED);
+ receiver.register();
+
+ FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"},
+ "", "", null, null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ final Intent intent = receiver.awaitForBroadcast(TimeUnit.MINUTES.toMillis(1));
+ assertNotNull("Failed to receive broadcast from VPN service", intent);
+ assertFalse("Wrong VpnService#isAlwaysOn",
+ intent.getBooleanExtra(MyVpnService.EXTRA_ALWAYS_ON, true));
+ assertFalse("Wrong VpnService#isLockdownEnabled",
+ intent.getBooleanExtra(MyVpnService.EXTRA_LOCKDOWN_ENABLED, true));
+
+ assertSocketClosed(fd, TEST_HOST);
+
+ checkTrafficOnVpn();
+
+ checkStrictModePrivateDns();
+
+ receiver.unregisterQuietly();
+ }
+
+ public void testAppAllowed() throws Exception {
+ if (!supportedHardware()) return;
+
+ FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
+
+ // 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"},
+ new String[] {"192.0.2.0/24", "2001:db8::/32"},
+ allowedApps, "", null, null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ assertSocketClosed(fd, TEST_HOST);
+
+ checkTrafficOnVpn();
+
+ checkStrictModePrivateDns();
+ }
+
+ public void testAppDisallowed() throws Exception {
+ if (!supportedHardware()) return;
+
+ FileDescriptor localFd = openSocketFd(TEST_HOST, 80, TIMEOUT_MS);
+ FileDescriptor remoteFd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
+
+ String disallowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
+ // If adb TCP port opened, this test may running by adb over TCP.
+ // Add com.android.shell appllication into blacklist to exclude adb socket for VPN test,
+ // see b/119382723.
+ // Note: The test don't support running adb over network for root device
+ disallowedApps = disallowedApps + ",com.android.shell";
+ 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 */);
+
+ assertSocketStillOpen(localFd, TEST_HOST);
+ assertSocketStillOpen(remoteFd, TEST_HOST);
+
+ checkNoTrafficOnVpn();
+ }
+
+ public void testGetConnectionOwnerUidSecurity() throws Exception {
+ if (!supportedHardware()) return;
+
+ DatagramSocket s;
+ InetAddress address = InetAddress.getByName("localhost");
+ s = new DatagramSocket();
+ s.setSoTimeout(SOCKET_TIMEOUT_MS);
+ s.connect(address, 7);
+ InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort());
+ InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort());
+ try {
+ int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_TCP, loc, rem);
+ fail("Only an active VPN app may call this API.");
+ } catch (SecurityException expected) {
+ return;
+ }
+ }
+
+ public void testSetProxy() throws Exception {
+ if (!supportedHardware()) return;
+ ProxyInfo initialProxy = mCM.getDefaultProxy();
+ // Receiver for the proxy change broadcast.
+ BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
+ proxyBroadcastReceiver.register();
+
+ String allowedApps = mPackageName;
+ ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888);
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "",
+ testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ // Check that the proxy change broadcast is received
+ try {
+ assertNotNull("No proxy change was broadcast when VPN is connected.",
+ proxyBroadcastReceiver.awaitForBroadcast());
+ } finally {
+ proxyBroadcastReceiver.unregisterQuietly();
+ }
+
+ // Proxy is set correctly in network and in link properties.
+ assertNetworkHasExpectedProxy(testProxyInfo, mNetwork);
+ assertDefaultProxy(testProxyInfo);
+
+ proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
+ proxyBroadcastReceiver.register();
+ stopVpn();
+ try {
+ assertNotNull("No proxy change was broadcast when VPN was disconnected.",
+ proxyBroadcastReceiver.awaitForBroadcast());
+ } finally {
+ proxyBroadcastReceiver.unregisterQuietly();
+ }
+
+ // After disconnecting from VPN, the proxy settings are the ones of the initial network.
+ assertDefaultProxy(initialProxy);
+ }
+
+ public void testSetProxyDisallowedApps() throws Exception {
+ if (!supportedHardware()) return;
+ ProxyInfo initialProxy = mCM.getDefaultProxy();
+
+ // If adb TCP port opened, this test may running by adb over TCP.
+ // Add com.android.shell appllication into blacklist to exclude adb socket for VPN test,
+ // see b/119382723.
+ // Note: The test don't support running adb over network for root device
+ String disallowedApps = mPackageName + ",com.android.shell";
+ ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888);
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, "", disallowedApps,
+ testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ // The disallowed app does has the proxy configs of the default network.
+ assertNetworkHasExpectedProxy(initialProxy, mCM.getActiveNetwork());
+ assertDefaultProxy(initialProxy);
+ }
+
+ public void testNoProxy() throws Exception {
+ if (!supportedHardware()) return;
+ ProxyInfo initialProxy = mCM.getDefaultProxy();
+ BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
+ proxyBroadcastReceiver.register();
+ String allowedApps = mPackageName;
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
+ null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ try {
+ assertNotNull("No proxy change was broadcast.",
+ proxyBroadcastReceiver.awaitForBroadcast());
+ } finally {
+ proxyBroadcastReceiver.unregisterQuietly();
+ }
+
+ // The VPN network has no proxy set.
+ assertNetworkHasExpectedProxy(null, mNetwork);
+
+ proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
+ proxyBroadcastReceiver.register();
+ stopVpn();
+ try {
+ assertNotNull("No proxy change was broadcast.",
+ proxyBroadcastReceiver.awaitForBroadcast());
+ } finally {
+ proxyBroadcastReceiver.unregisterQuietly();
+ }
+ // After disconnecting from VPN, the proxy settings are the ones of the initial network.
+ assertDefaultProxy(initialProxy);
+ assertNetworkHasExpectedProxy(initialProxy, mCM.getActiveNetwork());
+ }
+
+ public void testBindToNetworkWithProxy() throws Exception {
+ if (!supportedHardware()) return;
+ String allowedApps = mPackageName;
+ Network initialNetwork = mCM.getActiveNetwork();
+ ProxyInfo initialProxy = mCM.getDefaultProxy();
+ ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888);
+ // Receiver for the proxy change broadcast.
+ BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
+ proxyBroadcastReceiver.register();
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "",
+ testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ assertDefaultProxy(testProxyInfo);
+ mCM.bindProcessToNetwork(initialNetwork);
+ try {
+ assertNotNull("No proxy change was broadcast.",
+ proxyBroadcastReceiver.awaitForBroadcast());
+ } finally {
+ proxyBroadcastReceiver.unregisterQuietly();
+ }
+ assertDefaultProxy(initialProxy);
+ }
+
+ public void testVpnMeterednessWithNoUnderlyingNetwork() throws Exception {
+ if (!supportedHardware()) {
+ return;
+ }
+ // VPN is not routing any traffic i.e. its underlying networks is an empty array.
+ ArrayList<Network> underlyingNetworks = new ArrayList<>();
+ String allowedApps = mPackageName;
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
+ underlyingNetworks, false /* isAlwaysMetered */);
+
+ // VPN should now be the active network.
+ assertEquals(mNetwork, mCM.getActiveNetwork());
+ assertVpnTransportContains(NetworkCapabilities.TRANSPORT_VPN);
+ // VPN with no underlying networks should be metered by default.
+ assertTrue(isNetworkMetered(mNetwork));
+ assertTrue(mCM.isActiveNetworkMetered());
+ }
+
+ public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception {
+ if (!supportedHardware()) {
+ return;
+ }
+ Network underlyingNetwork = mCM.getActiveNetwork();
+ if (underlyingNetwork == null) {
+ Log.i(TAG, "testVpnMeterednessWithNullUnderlyingNetwork cannot execute"
+ + " unless there is an active network");
+ return;
+ }
+ // VPN tracks platform default.
+ ArrayList<Network> underlyingNetworks = null;
+ String allowedApps = mPackageName;
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
+ underlyingNetworks, false /*isAlwaysMetered */);
+
+ // Ensure VPN transports contains underlying network's transports.
+ assertVpnTransportContains(underlyingNetwork);
+ // Its meteredness should be same as that of underlying network.
+ assertEquals(isNetworkMetered(underlyingNetwork), isNetworkMetered(mNetwork));
+ // Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync.
+ assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered());
+ }
+
+ public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception {
+ if (!supportedHardware()) {
+ return;
+ }
+ Network underlyingNetwork = mCM.getActiveNetwork();
+ if (underlyingNetwork == null) {
+ Log.i(TAG, "testVpnMeterednessWithNonNullUnderlyingNetwork cannot execute"
+ + " unless there is an active network");
+ return;
+ }
+ // VPN explicitly declares WiFi to be its underlying network.
+ ArrayList<Network> underlyingNetworks = new ArrayList<>(1);
+ underlyingNetworks.add(underlyingNetwork);
+ String allowedApps = mPackageName;
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
+ underlyingNetworks, false /* isAlwaysMetered */);
+
+ // Ensure VPN transports contains underlying network's transports.
+ assertVpnTransportContains(underlyingNetwork);
+ // Its meteredness should be same as that of underlying network.
+ assertEquals(isNetworkMetered(underlyingNetwork), isNetworkMetered(mNetwork));
+ // Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync.
+ assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered());
+ }
+
+ public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception {
+ if (!supportedHardware()) {
+ return;
+ }
+ Network underlyingNetwork = mCM.getActiveNetwork();
+ if (underlyingNetwork == null) {
+ Log.i(TAG, "testAlwaysMeteredVpnWithNullUnderlyingNetwork cannot execute"
+ + " unless there is an active network");
+ return;
+ }
+ // VPN tracks platform default.
+ ArrayList<Network> underlyingNetworks = null;
+ String allowedApps = mPackageName;
+ boolean isAlwaysMetered = true;
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
+ underlyingNetworks, isAlwaysMetered);
+
+ // VPN's meteredness does not depend on underlying network since it is always metered.
+ assertTrue(isNetworkMetered(mNetwork));
+ assertTrue(mCM.isActiveNetworkMetered());
+ }
+
+ public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception {
+ if (!supportedHardware()) {
+ return;
+ }
+ Network underlyingNetwork = mCM.getActiveNetwork();
+ if (underlyingNetwork == null) {
+ Log.i(TAG, "testAlwaysMeteredVpnWithNonNullUnderlyingNetwork cannot execute"
+ + " unless there is an active network");
+ return;
+ }
+ // VPN explicitly declares its underlying network.
+ ArrayList<Network> underlyingNetworks = new ArrayList<>(1);
+ underlyingNetworks.add(underlyingNetwork);
+ String allowedApps = mPackageName;
+ boolean isAlwaysMetered = true;
+
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null,
+ underlyingNetworks, isAlwaysMetered);
+
+ // VPN's meteredness does not depend on underlying network since it is always metered.
+ assertTrue(isNetworkMetered(mNetwork));
+ assertTrue(mCM.isActiveNetworkMetered());
+ }
+
+ public void testB141603906() throws Exception {
+ if (!supportedHardware()) {
+ return;
+ }
+ final InetSocketAddress src = new InetSocketAddress(0);
+ final InetSocketAddress dst = new InetSocketAddress(0);
+ final int NUM_THREADS = 8;
+ final int NUM_SOCKETS = 5000;
+ final Thread[] threads = new Thread[NUM_THREADS];
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"0.0.0.0/0", "::/0"},
+ "" /* allowedApplications */, "com.android.shell" /* disallowedApplications */,
+ null /* proxyInfo */, null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ for (int i = 0; i < NUM_THREADS; i++) {
+ threads[i] = new Thread(() -> {
+ for (int j = 0; j < NUM_SOCKETS; j++) {
+ mCM.getConnectionOwnerUid(IPPROTO_TCP, src, dst);
+ }
+ });
+ }
+ for (Thread thread : threads) {
+ thread.start();
+ }
+ for (Thread thread : threads) {
+ thread.join();
+ }
+ stopVpn();
+ }
+
+ private boolean isNetworkMetered(Network network) {
+ NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+ return !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+ }
+
+ private void assertVpnTransportContains(Network underlyingNetwork) {
+ int[] transports = mCM.getNetworkCapabilities(underlyingNetwork).getTransportTypes();
+ assertVpnTransportContains(transports);
+ }
+
+ private void assertVpnTransportContains(int... transports) {
+ NetworkCapabilities vpnCaps = mCM.getNetworkCapabilities(mNetwork);
+ for (int transport : transports) {
+ assertTrue(vpnCaps.hasTransport(transport));
+ }
+ }
+
+ private void assertDefaultProxy(ProxyInfo expected) {
+ assertEquals("Incorrect proxy config.", expected, mCM.getDefaultProxy());
+ String expectedHost = expected == null ? null : expected.getHost();
+ String expectedPort = expected == null ? null : String.valueOf(expected.getPort());
+ assertEquals("Incorrect proxy host system property.", expectedHost,
+ System.getProperty("http.proxyHost"));
+ assertEquals("Incorrect proxy port system property.", expectedPort,
+ System.getProperty("http.proxyPort"));
+ }
+
+ private void assertNetworkHasExpectedProxy(ProxyInfo expected, Network network) {
+ LinkProperties lp = mCM.getLinkProperties(network);
+ assertNotNull("The network link properties object is null.", lp);
+ assertEquals("Incorrect proxy config.", expected, lp.getHttpProxy());
+
+ assertEquals(expected, mCM.getProxyForNetwork(network));
+ }
+
+ class ProxyChangeBroadcastReceiver extends BlockingBroadcastReceiver {
+ private boolean received;
+
+ public ProxyChangeBroadcastReceiver() {
+ super(VpnTest.this.getInstrumentation().getContext(), Proxy.PROXY_CHANGE_ACTION);
+ received = false;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!received) {
+ // Do not call onReceive() more than once.
+ super.onReceive(context, intent);
+ }
+ received = true;
+ }
+ }
+
+ /**
+ * Verifies that DownloadManager has CONNECTIVITY_USE_RESTRICTED_NETWORKS permission that can
+ * bind socket to VPN when it is in VPN disallowed list but requested downloading app is in VPN
+ * allowed list.
+ * See b/165774987.
+ */
+ public void testDownloadWithDownloadManagerDisallowed() throws Exception {
+ if (!supportedHardware()) return;
+
+ // Start a VPN with DownloadManager package in disallowed list.
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
+ new String[] {"192.0.2.0/24", "2001:db8::/32"},
+ "" /* allowedApps */, "com.android.providers.downloads", null /* proxyInfo */,
+ null /* underlyingNetworks */, false /* isAlwaysMetered */);
+
+ final Context context = VpnTest.this.getInstrumentation().getContext();
+ final DownloadManager dm = context.getSystemService(DownloadManager.class);
+ final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
+ try {
+ context.registerReceiver(receiver,
+ new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
+
+ // Enqueue a request and check only one download.
+ final long id = dm.enqueue(new Request(Uri.parse("https://www.google.com")));
+ assertEquals(1, getTotalNumberDownloads(dm, new Query()));
+ assertEquals(1, getTotalNumberDownloads(dm, new Query().setFilterById(id)));
+
+ // Wait for download complete and check status.
+ assertEquals(id, receiver.get(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(1, getTotalNumberDownloads(dm,
+ new Query().setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL)));
+
+ // Remove download.
+ assertEquals(1, dm.remove(id));
+ assertEquals(0, getTotalNumberDownloads(dm, new Query()));
+ } finally {
+ context.unregisterReceiver(receiver);
+ }
+ }
+
+ private static int getTotalNumberDownloads(final DownloadManager dm, final Query query) {
+ try (Cursor cursor = dm.query(query)) { return cursor.getCount(); }
+ }
+
+ private static class DownloadCompleteReceiver extends BroadcastReceiver {
+ private final CompletableFuture<Long> future = new CompletableFuture<>();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ future.complete(intent.getLongExtra(
+ DownloadManager.EXTRA_DOWNLOAD_ID, -1 /* defaultValue */));
+ }
+
+ public long get(long timeout, TimeUnit unit) throws Exception {
+ return future.get(timeout, unit);
+ }
+ }
+}
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
new file mode 100644
index 0000000..8e27931
--- /dev/null
+++ b/tests/cts/hostside/app2/Android.bp
@@ -0,0 +1,29 @@
+//
+// Copyright (C) 2016 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.
+//
+
+android_test_helper_app {
+ name: "CtsHostsideNetworkTestsApp2",
+ defaults: ["cts_support_defaults"],
+ sdk_version: "current",
+ static_libs: ["CtsHostsideNetworkTestsAidl"],
+ srcs: ["src/**/*.java"],
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+ certificate: ":cts-net-app",
+}
diff --git a/tests/cts/hostside/app2/AndroidManifest.xml b/tests/cts/hostside/app2/AndroidManifest.xml
new file mode 100644
index 0000000..ad270b3
--- /dev/null
+++ b/tests/cts/hostside/app2/AndroidManifest.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.cts.net.hostside.app2" >
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <!--
+ This application is used to listen to RESTRICT_BACKGROUND_CHANGED intents and store
+ them in a shared preferences which is then read by the test app. These broadcasts are
+ handled by 2 listeners, one defined the manifest and another dynamically registered by
+ a service.
+
+ The manifest-defined listener also handles ordered broadcasts used to share data with the
+ test app.
+
+ This application also provides a service, RemoteSocketFactoryService, that the test app can
+ use to open sockets to remote hosts as a different user ID.
+ -->
+ <application android:usesCleartextTraffic="true">
+ <activity android:name=".MyActivity" android:exported="true"/>
+ <service android:name=".MyService" android:exported="true"/>
+ <service android:name=".MyForegroundService" android:exported="true"/>
+ <service android:name=".RemoteSocketFactoryService" android:exported="true"/>
+
+ <receiver android:name=".MyBroadcastReceiver" >
+ <intent-filter>
+ <action android:name="android.net.conn.RESTRICT_BACKGROUND_CHANGED" />
+ <action android:name="com.android.cts.net.hostside.app2.action.GET_COUNTERS" />
+ <action android:name="com.android.cts.net.hostside.app2.action.GET_RESTRICT_BACKGROUND_STATUS" />
+ <action android:name="com.android.cts.net.hostside.app2.action.CHECK_NETWORK" />
+ <action android:name="com.android.cts.net.hostside.app2.action.SEND_NOTIFICATION" />
+ <action android:name="com.android.cts.net.hostside.app2.action.SHOW_TOAST" />
+ </intent-filter>
+ </receiver>
+ </application>
+
+</manifest>
diff --git a/tests/cts/hostside/app2/res/drawable/ic_notification.png b/tests/cts/hostside/app2/res/drawable/ic_notification.png
new file mode 100644
index 0000000..6ae570b
--- /dev/null
+++ b/tests/cts/hostside/app2/res/drawable/ic_notification.png
Binary files differ
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
new file mode 100644
index 0000000..351733e
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside.app2;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.cts.net.hostside.INetworkStateObserver;
+
+public final class Common {
+
+ static final String TAG = "CtsNetApp2";
+
+ // Constants below must match values defined on app's
+ // AbstractRestrictBackgroundNetworkTestCase.java
+ static final String MANIFEST_RECEIVER = "ManifestReceiver";
+ static final String DYNAMIC_RECEIVER = "DynamicReceiver";
+
+ static final String ACTION_RECEIVER_READY =
+ "com.android.cts.net.hostside.app2.action.RECEIVER_READY";
+ static final String ACTION_FINISH_ACTIVITY =
+ "com.android.cts.net.hostside.app2.action.FINISH_ACTIVITY";
+ static final String ACTION_SHOW_TOAST =
+ "com.android.cts.net.hostside.app2.action.SHOW_TOAST";
+
+ static final String NOTIFICATION_TYPE_CONTENT = "CONTENT";
+ static final String NOTIFICATION_TYPE_DELETE = "DELETE";
+ static final String NOTIFICATION_TYPE_FULL_SCREEN = "FULL_SCREEN";
+ static final String NOTIFICATION_TYPE_BUNDLE = "BUNDLE";
+ static final String NOTIFICATION_TYPE_ACTION = "ACTION";
+ static final String NOTIFICATION_TYPE_ACTION_BUNDLE = "ACTION_BUNDLE";
+ static final String NOTIFICATION_TYPE_ACTION_REMOTE_INPUT = "ACTION_REMOTE_INPUT";
+
+ static final String TEST_PKG = "com.android.cts.net.hostside";
+ static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
+
+ static int getUid(Context context) {
+ final String packageName = context.getPackageName();
+ try {
+ return context.getPackageManager().getPackageUid(packageName, 0);
+ } catch (NameNotFoundException e) {
+ throw new IllegalStateException("Could not get UID for " + packageName, e);
+ }
+ }
+
+ static void notifyNetworkStateObserver(Context context, Intent intent) {
+ if (intent == null) {
+ return;
+ }
+ final Bundle extras = intent.getExtras();
+ if (extras == null) {
+ return;
+ }
+ final INetworkStateObserver observer = INetworkStateObserver.Stub.asInterface(
+ extras.getBinder(KEY_NETWORK_STATE_OBSERVER));
+ if (observer != null) {
+ try {
+ if (!observer.isForeground()) {
+ Log.e(TAG, "App didn't come to foreground");
+ observer.onNetworkStateChecked(null);
+ return;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error occurred while reading the proc state: " + e);
+ }
+ AsyncTask.execute(() -> {
+ try {
+ observer.onNetworkStateChecked(
+ MyBroadcastReceiver.checkNetworkStatus(context));
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error occurred while notifying the observer: " + e);
+ }
+ });
+ }
+ }
+}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
new file mode 100644
index 0000000..286cc2f
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside.app2;
+
+import static com.android.cts.net.hostside.app2.Common.ACTION_FINISH_ACTIVITY;
+import static com.android.cts.net.hostside.app2.Common.TAG;
+import static com.android.cts.net.hostside.app2.Common.TEST_PKG;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.cts.net.hostside.INetworkStateObserver;
+
+/**
+ * Activity used to bring process to foreground.
+ */
+public class MyActivity extends Activity {
+
+ private BroadcastReceiver finishCommandReceiver = null;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.d(TAG, "MyActivity.onCreate()");
+ Common.notifyNetworkStateObserver(this, getIntent());
+ finishCommandReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "Finishing MyActivity");
+ MyActivity.this.finish();
+ }
+ };
+ registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY));
+ }
+
+ @Override
+ public void finish() {
+ if (finishCommandReceiver != null) {
+ unregisterReceiver(finishCommandReceiver);
+ }
+ super.finish();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ Log.d(TAG, "MyActivity.onStart()");
+ }
+
+ @Override
+ protected void onDestroy() {
+ Log.d(TAG, "MyActivity.onDestroy()");
+ super.onDestroy();
+ }
+}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
new file mode 100644
index 0000000..aa54075
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside.app2;
+
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
+
+import static com.android.cts.net.hostside.app2.Common.ACTION_RECEIVER_READY;
+import static com.android.cts.net.hostside.app2.Common.ACTION_SHOW_TOAST;
+import static com.android.cts.net.hostside.app2.Common.MANIFEST_RECEIVER;
+import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_ACTION;
+import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_BUNDLE;
+import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_REMOTE_INPUT;
+import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_BUNDLE;
+import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_CONTENT;
+import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_DELETE;
+import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_FULL_SCREEN;
+import static com.android.cts.net.hostside.app2.Common.TAG;
+import static com.android.cts.net.hostside.app2.Common.getUid;
+
+import android.app.Notification;
+import android.app.Notification.Action;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/**
+ * Receiver used to:
+ * <ol>
+ * <li>Count number of {@code RESTRICT_BACKGROUND_CHANGED} broadcasts received.
+ * <li>Show a toast.
+ * </ol>
+ */
+public class MyBroadcastReceiver extends BroadcastReceiver {
+
+ private static final int NETWORK_TIMEOUT_MS = 5 * 1000;
+
+ private final String mName;
+
+ public MyBroadcastReceiver() {
+ this(MANIFEST_RECEIVER);
+ }
+
+ MyBroadcastReceiver(String name) {
+ Log.d(TAG, "Constructing MyBroadcastReceiver named " + name);
+ mName = name;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "onReceive() for " + mName + ": " + intent);
+ final String action = intent.getAction();
+ switch (action) {
+ case ACTION_RESTRICT_BACKGROUND_CHANGED:
+ increaseCounter(context, action);
+ break;
+ case ACTION_RECEIVER_READY:
+ final String message = mName + " is ready to rumble";
+ Log.d(TAG, message);
+ setResultData(message);
+ break;
+ case ACTION_SHOW_TOAST:
+ showToast(context);
+ break;
+ default:
+ Log.e(TAG, "received unexpected action: " + action);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "[MyBroadcastReceiver: mName=" + mName + "]";
+ }
+
+ private void increaseCounter(Context context, String action) {
+ final SharedPreferences prefs = context.getApplicationContext()
+ .getSharedPreferences(mName, Context.MODE_PRIVATE);
+ final int value = prefs.getInt(action, 0) + 1;
+ Log.d(TAG, "increaseCounter('" + action + "'): setting '" + mName + "' to " + value);
+ prefs.edit().putInt(action, value).apply();
+ }
+
+ static int getCounter(Context context, String action, String receiverName) {
+ final SharedPreferences prefs = context.getSharedPreferences(receiverName,
+ Context.MODE_PRIVATE);
+ final int value = prefs.getInt(action, 0);
+ Log.d(TAG, "getCounter('" + action + "', '" + receiverName + "'): " + value);
+ return value;
+ }
+
+ static String getRestrictBackgroundStatus(Context context) {
+ final ConnectivityManager cm = (ConnectivityManager) context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ final int apiStatus = cm.getRestrictBackgroundStatus();
+ Log.d(TAG, "getRestrictBackgroundStatus: returning " + apiStatus);
+ return String.valueOf(apiStatus);
+ }
+
+ private static final String NETWORK_STATUS_TEMPLATE = "%s|%s|%s|%s|%s";
+ /**
+ * Checks whether the network is available and return a string which can then be send as a
+ * result data for the ordered broadcast.
+ *
+ * <p>
+ * The string has the following format:
+ *
+ * <p><pre><code>
+ * NetinfoState|NetinfoDetailedState|RealConnectionCheck|RealConnectionCheckDetails|Netinfo
+ * </code></pre>
+ *
+ * <p>Where:
+ *
+ * <ul>
+ * <li>{@code NetinfoState}: enum value of {@link NetworkInfo.State}.
+ * <li>{@code NetinfoDetailedState}: enum value of {@link NetworkInfo.DetailedState}.
+ * <li>{@code RealConnectionCheck}: boolean value of a real connection check (i.e., an attempt
+ * to access an external website.
+ * <li>{@code RealConnectionCheckDetails}: if HTTP output core or exception string of the real
+ * connection attempt
+ * <li>{@code Netinfo}: string representation of the {@link NetworkInfo}.
+ * </ul>
+ *
+ * For example, if the connection was established fine, the result would be something like:
+ * <p><pre><code>
+ * CONNECTED|CONNECTED|true|200|[type: WIFI[], state: CONNECTED/CONNECTED, reason: ...]
+ * </code></pre>
+ *
+ */
+ // TODO: now that it uses Binder, it counl return a Bundle with the data parts instead...
+ static String checkNetworkStatus(Context context) {
+ final ConnectivityManager cm =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ // TODO: connect to a hostside server instead
+ final String address = "http://example.com";
+ final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+ Log.d(TAG, "Running checkNetworkStatus() on thread "
+ + Thread.currentThread().getName() + " for UID " + getUid(context)
+ + "\n\tactiveNetworkInfo: " + networkInfo + "\n\tURL: " + address);
+ boolean checkStatus = false;
+ String checkDetails = "N/A";
+ try {
+ final URL url = new URL(address);
+ final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setReadTimeout(NETWORK_TIMEOUT_MS);
+ conn.setConnectTimeout(NETWORK_TIMEOUT_MS / 2);
+ conn.setRequestMethod("GET");
+ conn.setDoInput(true);
+ conn.connect();
+ final int response = conn.getResponseCode();
+ checkStatus = true;
+ checkDetails = "HTTP response for " + address + ": " + response;
+ } catch (Exception e) {
+ checkStatus = false;
+ checkDetails = "Exception getting " + address + ": " + e;
+ }
+ Log.d(TAG, checkDetails);
+ final String state, detailedState;
+ if (networkInfo != null) {
+ state = networkInfo.getState().name();
+ detailedState = networkInfo.getDetailedState().name();
+ } else {
+ state = detailedState = "null";
+ }
+ final String status = String.format(NETWORK_STATUS_TEMPLATE, state, detailedState,
+ Boolean.valueOf(checkStatus), checkDetails, networkInfo);
+ Log.d(TAG, "Offering " + status);
+ return status;
+ }
+
+ /**
+ * Sends a system notification containing actions with pending intents to launch the app's
+ * main activitiy or service.
+ */
+ static void sendNotification(Context context, String channelId, int notificationId,
+ String notificationType ) {
+ Log.d(TAG, "sendNotification: id=" + notificationId + ", type=" + notificationType);
+ final Intent serviceIntent = new Intent(context, MyService.class);
+ final PendingIntent pendingIntent = PendingIntent.getService(context, 0, serviceIntent,
+ notificationId);
+ final Bundle bundle = new Bundle();
+ bundle.putCharSequence("parcelable", "I am not");
+
+ final Notification.Builder builder = new Notification.Builder(context, channelId)
+ .setSmallIcon(R.drawable.ic_notification);
+
+ Action action = null;
+ switch (notificationType) {
+ case NOTIFICATION_TYPE_CONTENT:
+ builder
+ .setContentTitle("Light, Cameras...")
+ .setContentIntent(pendingIntent);
+ break;
+ case NOTIFICATION_TYPE_DELETE:
+ builder.setDeleteIntent(pendingIntent);
+ break;
+ case NOTIFICATION_TYPE_FULL_SCREEN:
+ builder.setFullScreenIntent(pendingIntent, true);
+ break;
+ case NOTIFICATION_TYPE_BUNDLE:
+ bundle.putParcelable("Magnum P.I. (Pending Intent)", pendingIntent);
+ builder.setExtras(bundle);
+ break;
+ case NOTIFICATION_TYPE_ACTION:
+ action = new Action.Builder(
+ R.drawable.ic_notification, "ACTION", pendingIntent)
+ .build();
+ builder.addAction(action);
+ break;
+ case NOTIFICATION_TYPE_ACTION_BUNDLE:
+ bundle.putParcelable("Magnum A.P.I. (Action Pending Intent)", pendingIntent);
+ action = new Action.Builder(
+ R.drawable.ic_notification, "ACTION WITH BUNDLE", null)
+ .addExtras(bundle)
+ .build();
+ builder.addAction(action);
+ break;
+ case NOTIFICATION_TYPE_ACTION_REMOTE_INPUT:
+ bundle.putParcelable("Magnum R.I. (Remote Input)", null);
+ final RemoteInput remoteInput = new RemoteInput.Builder("RI")
+ .addExtras(bundle)
+ .build();
+ action = new Action.Builder(
+ R.drawable.ic_notification, "ACTION WITH REMOTE INPUT", pendingIntent)
+ .addRemoteInput(remoteInput)
+ .build();
+ builder.addAction(action);
+ break;
+ default:
+ Log.e(TAG, "Unknown notification type: " + notificationType);
+ return;
+ }
+
+ final Notification notification = builder.build();
+ ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
+ .notify(notificationId, notification);
+ }
+
+ private void showToast(Context context) {
+ Toast.makeText(context, "Toast from CTS test", Toast.LENGTH_SHORT).show();
+ setResultData("Shown");
+ }
+}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java
new file mode 100644
index 0000000..ff4ba65
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside.app2;
+
+import static com.android.cts.net.hostside.app2.Common.TAG;
+import static com.android.cts.net.hostside.app2.Common.TEST_PKG;
+
+import android.R;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.cts.net.hostside.INetworkStateObserver;
+
+/**
+ * Service used to change app state to FOREGROUND_SERVICE.
+ */
+public class MyForegroundService extends Service {
+ private static final String NOTIFICATION_CHANNEL_ID = "cts/MyForegroundService";
+ private static final int FLAG_START_FOREGROUND = 1;
+ private static final int FLAG_STOP_FOREGROUND = 2;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.v(TAG, "MyForegroundService.onStartCommand(): " + intent);
+ NotificationManager notificationManager = getSystemService(NotificationManager.class);
+ notificationManager.createNotificationChannel(new NotificationChannel(
+ NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID,
+ NotificationManager.IMPORTANCE_DEFAULT));
+ switch (intent.getFlags()) {
+ case FLAG_START_FOREGROUND:
+ Log.d(TAG, "Starting foreground");
+ startForeground(42, new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_dialog_alert) // any icon is fine
+ .build());
+ Common.notifyNetworkStateObserver(this, intent);
+ break;
+ case FLAG_STOP_FOREGROUND:
+ Log.d(TAG, "Stopping foreground");
+ stopForeground(true);
+ break;
+ default:
+ Log.wtf(TAG, "Invalid flag on intent " + intent);
+ }
+ return START_STICKY;
+ }
+}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
new file mode 100644
index 0000000..590e17e
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside.app2;
+
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
+
+import static com.android.cts.net.hostside.app2.Common.ACTION_RECEIVER_READY;
+import static com.android.cts.net.hostside.app2.Common.DYNAMIC_RECEIVER;
+import static com.android.cts.net.hostside.app2.Common.TAG;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.cts.net.hostside.IMyService;
+import com.android.cts.net.hostside.INetworkCallback;
+
+/**
+ * Service used to dynamically register a broadcast receiver.
+ */
+public class MyService extends Service {
+ private static final String NOTIFICATION_CHANNEL_ID = "MyService";
+
+ ConnectivityManager mCm;
+
+ private MyBroadcastReceiver mReceiver;
+ private ConnectivityManager.NetworkCallback mNetworkCallback;
+
+ // TODO: move MyBroadcast static functions here - they were kept there to make git diff easier.
+
+ private IMyService.Stub mBinder =
+ new IMyService.Stub() {
+
+ @Override
+ public void registerBroadcastReceiver() {
+ if (mReceiver != null) {
+ Log.d(TAG, "receiver already registered: " + mReceiver);
+ return;
+ }
+ final Context context = getApplicationContext();
+ mReceiver = new MyBroadcastReceiver(DYNAMIC_RECEIVER);
+ context.registerReceiver(mReceiver, new IntentFilter(ACTION_RECEIVER_READY));
+ context.registerReceiver(mReceiver,
+ new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED));
+ Log.d(TAG, "receiver registered");
+ }
+
+ @Override
+ public int getCounters(String receiverName, String action) {
+ return MyBroadcastReceiver.getCounter(getApplicationContext(), action, receiverName);
+ }
+
+ @Override
+ public String checkNetworkStatus() {
+ return MyBroadcastReceiver.checkNetworkStatus(getApplicationContext());
+ }
+
+ @Override
+ public String getRestrictBackgroundStatus() {
+ return MyBroadcastReceiver.getRestrictBackgroundStatus(getApplicationContext());
+ }
+
+ @Override
+ public void sendNotification(int notificationId, String notificationType) {
+ MyBroadcastReceiver .sendNotification(getApplicationContext(), NOTIFICATION_CHANNEL_ID,
+ notificationId, notificationType);
+ }
+
+ @Override
+ public void registerNetworkCallback(INetworkCallback cb) {
+ if (mNetworkCallback != null) {
+ Log.d(TAG, "unregister previous network callback: " + mNetworkCallback);
+ unregisterNetworkCallback();
+ }
+ Log.d(TAG, "registering network callback");
+
+ mNetworkCallback = new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onBlockedStatusChanged(Network network, boolean blocked) {
+ try {
+ cb.onBlockedStatusChanged(network, blocked);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Cannot send onBlockedStatusChanged: " + e);
+ unregisterNetworkCallback();
+ }
+ }
+
+ @Override
+ public void onAvailable(Network network) {
+ try {
+ cb.onAvailable(network);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Cannot send onAvailable: " + e);
+ unregisterNetworkCallback();
+ }
+ }
+
+ @Override
+ public void onLost(Network network) {
+ try {
+ cb.onLost(network);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Cannot send onLost: " + e);
+ unregisterNetworkCallback();
+ }
+ }
+
+ @Override
+ public void onCapabilitiesChanged(Network network, NetworkCapabilities cap) {
+ try {
+ cb.onCapabilitiesChanged(network, cap);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Cannot send onCapabilitiesChanged: " + e);
+ unregisterNetworkCallback();
+ }
+ }
+ };
+ mCm.registerNetworkCallback(makeWifiNetworkRequest(), mNetworkCallback);
+ try {
+ cb.asBinder().linkToDeath(() -> unregisterNetworkCallback(), 0);
+ } catch (RemoteException e) {
+ unregisterNetworkCallback();
+ }
+ }
+
+ @Override
+ public void unregisterNetworkCallback() {
+ Log.d(TAG, "unregistering network callback");
+ if (mNetworkCallback != null) {
+ mCm.unregisterNetworkCallback(mNetworkCallback);
+ mNetworkCallback = null;
+ }
+ }
+ };
+
+ private NetworkRequest makeWifiNetworkRequest() {
+ return new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public void onCreate() {
+ final Context context = getApplicationContext();
+ ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
+ .createNotificationChannel(new NotificationChannel(NOTIFICATION_CHANNEL_ID,
+ NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT));
+ mCm = (ConnectivityManager) getApplicationContext()
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ @Override
+ public void onDestroy() {
+ final Context context = getApplicationContext();
+ ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
+ .deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
+ if (mReceiver != null) {
+ Log.d(TAG, "onDestroy(): unregistering " + mReceiver);
+ getApplicationContext().unregisterReceiver(mReceiver);
+ }
+
+ super.onDestroy();
+ }
+}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java
new file mode 100644
index 0000000..b1b7d77
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net.hostside.app2;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.util.Log;
+
+import com.android.cts.net.hostside.IRemoteSocketFactory;
+
+import java.net.Socket;
+
+
+public class RemoteSocketFactoryService extends Service {
+
+ private static final String TAG = RemoteSocketFactoryService.class.getSimpleName();
+
+ private IRemoteSocketFactory.Stub mBinder = new IRemoteSocketFactory.Stub() {
+ @Override
+ public ParcelFileDescriptor openSocketFd(String host, int port, int timeoutMs) {
+ try {
+ Socket s = new Socket(host, port);
+ s.setSoTimeout(timeoutMs);
+ return ParcelFileDescriptor.fromSocket(s);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @Override
+ public String getPackageName() {
+ return RemoteSocketFactoryService.this.getPackageName();
+ }
+
+ @Override
+ public int getUid() {
+ return Process.myUid();
+ }
+ };
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+}
diff --git a/tests/cts/hostside/certs/Android.bp b/tests/cts/hostside/certs/Android.bp
new file mode 100644
index 0000000..ab4cf34
--- /dev/null
+++ b/tests/cts/hostside/certs/Android.bp
@@ -0,0 +1,4 @@
+android_app_certificate {
+ name: "cts-net-app",
+ certificate: "cts-net-app",
+}
diff --git a/tests/cts/hostside/certs/README b/tests/cts/hostside/certs/README
new file mode 100644
index 0000000..b660a82
--- /dev/null
+++ b/tests/cts/hostside/certs/README
@@ -0,0 +1,2 @@
+# Generated with:
+development/tools/make_key cts-net-app '/CN=cts-net-app'
diff --git a/tests/cts/hostside/certs/cts-net-app.pk8 b/tests/cts/hostside/certs/cts-net-app.pk8
new file mode 100644
index 0000000..1703e4e
--- /dev/null
+++ b/tests/cts/hostside/certs/cts-net-app.pk8
Binary files differ
diff --git a/tests/cts/hostside/certs/cts-net-app.x509.pem b/tests/cts/hostside/certs/cts-net-app.x509.pem
new file mode 100644
index 0000000..a15ff48
--- /dev/null
+++ b/tests/cts/hostside/certs/cts-net-app.x509.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDAjCCAeqgAwIBAgIJAMhWwIIqr1r6MA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV
+BAMMC2N0cy1uZXQtYXBwMB4XDTE4MDYyMDAyMjAwN1oXDTQ1MTEwNTAyMjAwN1ow
+FjEUMBIGA1UEAwwLY3RzLW5ldC1hcHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQDefOayWQss1E+FQIONK6IhlXhe0BEyHshIrnPOOmuCPa/Svfbnmziy
+hr1KTjaQ3ET/mGShwlt6AUti7nKx9aB71IJp5mSBuwW62A8jvN3yNOo45YV8+n1o
+TrEoMWMf7hQmoOSqaSJ+VFuVms/kPSEh99okDgHCej6rsEkEcDoh6pJajQyUYDwR
+SNAF8SrqCDhqFbZW/LWedvuikCUlNtzuv7/GrcLcsiWEfHv7UOBKpMjLo9BhD1XF
+IefnxImcBQrQGMnE9TLixBiEeX5yauLgbZuxBqD/zsI2TH1FjxTeuJan83kLbqqH
+FgyvPaUjwckAdQPyom7ZUYFnBc0LQ9xzAgMBAAGjUzBRMB0GA1UdDgQWBBRZrBEw
+tAB2WNXj8dQ7ZOuJ34kY5DAfBgNVHSMEGDAWgBRZrBEwtAB2WNXj8dQ7ZOuJ34kY
+5DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDeI9AnLW6l/39y
+z96w/ldxZVFPzBRiFIsJsPHVyXlD5vUHZv/ju2jFn8TZSZR5TK0bzCEoVLp34Sho
+bbS0magP82yIvCRibyoyD+TDNnZkNJwjYnikE+/oyshTSQtpkn/rDA+0Y09BUC1E
+N2I6bV9pTXLFg7oah2FmqPRPzhgeYUKENgOQkrrjUCn6y0i/k374n7aftzdniSIz
+2kCRVEeN9gws6CnoMPx0vr32v/JVuPV6zfdJYadgj/eFRyTNE4msd9kE82Wc46eU
+YiI+LuXZ3ZMUNWGY7MK2pOUUS52JsBQ3K235dA5WaU4x8OBlY/WkNYX/eLbNs5jj
+FzLmhZZ1
+-----END CERTIFICATE-----
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
new file mode 100644
index 0000000..1312085
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2019 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 com.android.cts.net;
+public class HostsideNetworkCallbackTests extends HostsideNetworkTestCase {
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ uninstallPackage(TEST_APP2_PKG, false);
+ installPackage(TEST_APP2_APK);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ uninstallPackage(TEST_APP2_PKG, true);
+ }
+
+ public void testOnBlockedStatusChanged_dataSaver() throws Exception {
+ runDeviceTests(TEST_PKG,
+ TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_dataSaver");
+ }
+
+ public void testOnBlockedStatusChanged_powerSaver() throws Exception {
+ runDeviceTests(TEST_PKG,
+ TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_powerSaver");
+ }
+}
+
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
new file mode 100644
index 0000000..ce20379
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.result.CollectingTestListener;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.TestResult;
+import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IAbi;
+import com.android.tradefed.testtype.IAbiReceiver;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.io.FileNotFoundException;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+abstract class HostsideNetworkTestCase extends DeviceTestCase implements IAbiReceiver,
+ IBuildReceiver {
+ protected static final boolean DEBUG = false;
+ 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_APP2_PKG = "com.android.cts.net.hostside.app2";
+ protected static final String TEST_APP2_APK = "CtsHostsideNetworkTestsApp2.apk";
+
+ private IAbi mAbi;
+ private IBuildInfo mCtsBuild;
+
+ @Override
+ public void setAbi(IAbi abi) {
+ mAbi = abi;
+ }
+
+ @Override
+ public void setBuild(IBuildInfo buildInfo) {
+ mCtsBuild = buildInfo;
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ assertNotNull(mAbi);
+ assertNotNull(mCtsBuild);
+
+ uninstallPackage(TEST_PKG, false);
+ installPackage(TEST_APK);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ uninstallPackage(TEST_PKG, true);
+ }
+
+ protected void installPackage(String apk) throws FileNotFoundException,
+ DeviceNotAvailableException {
+ CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
+ assertNull(getDevice().installPackage(buildHelper.getTestFile(apk),
+ false /* reinstall */, true /* grantPermissions */));
+ }
+
+ protected void uninstallPackage(String packageName, boolean shouldSucceed)
+ throws DeviceNotAvailableException {
+ final String result = getDevice().uninstallPackage(packageName);
+ if (shouldSucceed) {
+ assertNull("uninstallPackage(" + packageName + ") failed: " + result, result);
+ }
+ }
+
+ protected void assertPackageUninstalled(String packageName) throws DeviceNotAvailableException,
+ InterruptedException {
+ final String command = "cmd package list packages " + packageName;
+ final int max_tries = 5;
+ for (int i = 1; i <= max_tries; i++) {
+ final String result = runCommand(command);
+ if (result.trim().isEmpty()) {
+ return;
+ }
+ // 'list packages' filters by substring, so we need to iterate with the results
+ // and check one by one, otherwise 'com.android.cts.net.hostside' could return
+ // 'com.android.cts.net.hostside.app2'
+ boolean found = false;
+ for (String line : result.split("[\\r\\n]+")) {
+ if (line.endsWith(packageName)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return;
+ }
+ i++;
+ Log.v(TAG, "Package " + packageName + " not uninstalled yet (" + result
+ + "); sleeping 1s before polling again");
+ Thread.sleep(1000);
+ }
+ fail("Package '" + packageName + "' not uinstalled after " + max_tries + " seconds");
+ }
+
+ protected void runDeviceTests(String packageName, String testClassName)
+ throws DeviceNotAvailableException {
+ runDeviceTests(packageName, testClassName, null);
+ }
+
+ protected void runDeviceTests(String packageName, String testClassName, String methodName)
+ throws DeviceNotAvailableException {
+ RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(packageName,
+ "androidx.test.runner.AndroidJUnitRunner", getDevice().getIDevice());
+
+ if (testClassName != null) {
+ if (methodName != null) {
+ testRunner.setMethodName(testClassName, methodName);
+ } else {
+ testRunner.setClassName(testClassName);
+ }
+ }
+
+ final CollectingTestListener listener = new CollectingTestListener();
+ getDevice().runInstrumentationTests(testRunner, listener);
+
+ final TestRunResult result = listener.getCurrentRunResults();
+ if (result.isRunFailure()) {
+ throw new AssertionError("Failed to successfully run device tests for "
+ + result.getName() + ": " + result.getRunFailureMessage());
+ }
+
+ if (result.hasFailedTests()) {
+ // build a meaningful error message
+ StringBuilder errorBuilder = new StringBuilder("on-device tests failed:\n");
+ for (Map.Entry<TestDescription, TestResult> resultEntry :
+ result.getTestResults().entrySet()) {
+ if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
+ errorBuilder.append(resultEntry.getKey().toString());
+ errorBuilder.append(":\n");
+ errorBuilder.append(resultEntry.getValue().getStackTrace());
+ }
+ }
+ throw new AssertionError(errorBuilder.toString());
+ }
+ }
+
+ private static final Pattern UID_PATTERN =
+ Pattern.compile(".*userId=([0-9]+)$", Pattern.MULTILINE);
+
+ protected int getUid(String packageName) throws DeviceNotAvailableException {
+ final String output = runCommand("dumpsys package " + packageName);
+ final Matcher matcher = UID_PATTERN.matcher(output);
+ while (matcher.find()) {
+ final String match = matcher.group(1);
+ return Integer.parseInt(match);
+ }
+ throw new RuntimeException("Did not find regexp '" + UID_PATTERN + "' on adb output\n"
+ + output);
+ }
+
+ protected String runCommand(String command) throws DeviceNotAvailableException {
+ Log.d(TAG, "Command: '" + command + "'");
+ final String output = getDevice().executeShellCommand(command);
+ if (DEBUG) Log.v(TAG, "Output: " + output.trim());
+ return output;
+ }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
new file mode 100644
index 0000000..ac28c7a
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net;
+
+import com.android.ddmlib.Log;
+import com.android.tradefed.device.DeviceNotAvailableException;
+
+public class HostsideRestrictBackgroundNetworkTests extends HostsideNetworkTestCase {
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ uninstallPackage(TEST_APP2_PKG, false);
+ installPackage(TEST_APP2_APK);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ uninstallPackage(TEST_APP2_PKG, true);
+ }
+
+ /**************************
+ * Data Saver Mode tests. *
+ **************************/
+
+ public void testDataSaverMode_disabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ "testGetRestrictBackgroundStatus_disabled");
+ }
+
+ public void testDataSaverMode_whitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ "testGetRestrictBackgroundStatus_whitelisted");
+ }
+
+ public void testDataSaverMode_enabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ "testGetRestrictBackgroundStatus_enabled");
+ }
+
+ public void testDataSaverMode_blacklisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ "testGetRestrictBackgroundStatus_blacklisted");
+ }
+
+ public void testDataSaverMode_reinstall() throws Exception {
+ final int oldUid = getUid(TEST_APP2_PKG);
+
+ // Make sure whitelist is revoked when package is removed
+ addRestrictBackgroundWhitelist(oldUid);
+
+ uninstallPackage(TEST_APP2_PKG, true);
+ assertPackageUninstalled(TEST_APP2_PKG);
+ assertRestrictBackgroundWhitelist(oldUid, false);
+
+ installPackage(TEST_APP2_APK);
+ final int newUid = getUid(TEST_APP2_PKG);
+ assertRestrictBackgroundWhitelist(oldUid, false);
+ assertRestrictBackgroundWhitelist(newUid, false);
+ }
+
+ public void testDataSaverMode_requiredWhitelistedPackages() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ "testGetRestrictBackgroundStatus_requiredWhitelistedPackages");
+ }
+
+ public void testDataSaverMode_broadcastNotSentOnUnsupportedDevices() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ "testBroadcastNotSentOnUnsupportedDevices");
+ }
+
+ /*****************************
+ * Battery Saver Mode tests. *
+ *****************************/
+
+ public void testBatterySaverModeMetered_disabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
+ "testBackgroundNetworkAccess_disabled");
+ }
+
+ public void testBatterySaverModeMetered_whitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
+ "testBackgroundNetworkAccess_whitelisted");
+ }
+
+ public void testBatterySaverModeMetered_enabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
+ "testBackgroundNetworkAccess_enabled");
+ }
+
+ public void testBatterySaverMode_reinstall() throws Exception {
+ if (!isDozeModeEnabled()) {
+ Log.w(TAG, "testBatterySaverMode_reinstall() skipped because device does not support "
+ + "Doze Mode");
+ return;
+ }
+
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+
+ uninstallPackage(TEST_APP2_PKG, true);
+ assertPackageUninstalled(TEST_APP2_PKG);
+ assertPowerSaveModeWhitelist(TEST_APP2_PKG, false);
+
+ installPackage(TEST_APP2_APK);
+ assertPowerSaveModeWhitelist(TEST_APP2_PKG, false);
+ }
+
+ public void testBatterySaverModeNonMetered_disabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+ "testBackgroundNetworkAccess_disabled");
+ }
+
+ public void testBatterySaverModeNonMetered_whitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+ "testBackgroundNetworkAccess_whitelisted");
+ }
+
+ public void testBatterySaverModeNonMetered_enabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+ "testBackgroundNetworkAccess_enabled");
+ }
+
+ /*******************
+ * App idle tests. *
+ *******************/
+
+ public void testAppIdleMetered_disabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ "testBackgroundNetworkAccess_disabled");
+ }
+
+ public void testAppIdleMetered_whitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ "testBackgroundNetworkAccess_whitelisted");
+ }
+
+ public void testAppIdleMetered_tempWhitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ "testBackgroundNetworkAccess_tempWhitelisted");
+ }
+
+ public void testAppIdleMetered_enabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ "testBackgroundNetworkAccess_enabled");
+ }
+
+ public void testAppIdleMetered_idleWhitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ "testAppIdleNetworkAccess_idleWhitelisted");
+ }
+
+ // TODO: currently power-save mode and idle uses the same whitelist, so this test would be
+ // redundant (as it would be testing the same as testBatterySaverMode_reinstall())
+ // public void testAppIdle_reinstall() throws Exception {
+ // }
+
+ public void testAppIdleNonMetered_disabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ "testBackgroundNetworkAccess_disabled");
+ }
+
+ public void testAppIdleNonMetered_whitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ "testBackgroundNetworkAccess_whitelisted");
+ }
+
+ public void testAppIdleNonMetered_tempWhitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ "testBackgroundNetworkAccess_tempWhitelisted");
+ }
+
+ public void testAppIdleNonMetered_enabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ "testBackgroundNetworkAccess_enabled");
+ }
+
+ public void testAppIdleNonMetered_idleWhitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ "testAppIdleNetworkAccess_idleWhitelisted");
+ }
+
+ public void testAppIdleNonMetered_whenCharging() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ "testAppIdleNetworkAccess_whenCharging");
+ }
+
+ public void testAppIdleMetered_whenCharging() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ "testAppIdleNetworkAccess_whenCharging");
+ }
+
+ public void testAppIdle_toast() throws Exception {
+ // Check that showing a toast doesn't bring an app out of standby
+ runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ "testAppIdle_toast");
+ }
+
+ /********************
+ * Doze Mode tests. *
+ ********************/
+
+ public void testDozeModeMetered_disabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+ "testBackgroundNetworkAccess_disabled");
+ }
+
+ public void testDozeModeMetered_whitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+ "testBackgroundNetworkAccess_whitelisted");
+ }
+
+ public void testDozeModeMetered_enabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+ "testBackgroundNetworkAccess_enabled");
+ }
+
+ public void testDozeModeMetered_enabledButWhitelistedOnNotificationAction() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+ "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
+ }
+
+ // TODO: currently power-save mode and idle uses the same whitelist, so this test would be
+ // redundant (as it would be testing the same as testBatterySaverMode_reinstall())
+ // public void testDozeMode_reinstall() throws Exception {
+ // }
+
+ public void testDozeModeNonMetered_disabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+ "testBackgroundNetworkAccess_disabled");
+ }
+
+ public void testDozeModeNonMetered_whitelisted() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+ "testBackgroundNetworkAccess_whitelisted");
+ }
+
+ public void testDozeModeNonMetered_enabled() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+ "testBackgroundNetworkAccess_enabled");
+ }
+
+ public void testDozeModeNonMetered_enabledButWhitelistedOnNotificationAction()
+ throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+ "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
+ }
+
+ /**********************
+ * Mixed modes tests. *
+ **********************/
+
+ public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testDataAndBatterySaverModes_meteredNetwork");
+ }
+
+ public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testDataAndBatterySaverModes_nonMeteredNetwork");
+ }
+
+ public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testDozeAndBatterySaverMode_powerSaveWhitelists");
+ }
+
+ public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testDozeAndAppIdle_powerSaveWhitelists");
+ }
+
+ public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testAppIdleAndDoze_tempPowerSaveWhitelists");
+ }
+
+ public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testAppIdleAndBatterySaver_tempPowerSaveWhitelists");
+ }
+
+ public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testDozeAndAppIdle_appIdleWhitelist");
+ }
+
+ public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists");
+ }
+
+ public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ "testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists");
+ }
+
+ /*******************
+ * Helper methods. *
+ *******************/
+
+ private void assertRestrictBackgroundWhitelist(int uid, boolean expected) throws Exception {
+ final int max_tries = 5;
+ boolean actual = false;
+ for (int i = 1; i <= max_tries; i++) {
+ final String output = runCommand("cmd netpolicy list restrict-background-whitelist ");
+ actual = output.contains(Integer.toString(uid));
+ if (expected == actual) {
+ return;
+ }
+ Log.v(TAG, "whitelist check for uid " + uid + " doesn't match yet (expected "
+ + expected + ", got " + actual + "); sleeping 1s before polling again");
+ Thread.sleep(1000);
+ }
+ fail("whitelist check for uid " + uid + " failed: expected "
+ + expected + ", got " + actual);
+ }
+
+ private void assertPowerSaveModeWhitelist(String packageName, boolean expected)
+ throws Exception {
+ // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
+ // need to use netpolicy for whitelisting
+ assertDelayedCommand("dumpsys deviceidle whitelist =" + packageName,
+ Boolean.toString(expected));
+ }
+
+ /**
+ * Asserts the result of a command, wait and re-running it a couple times if necessary.
+ */
+ private void assertDelayedCommand(String command, String expectedResult)
+ throws InterruptedException, DeviceNotAvailableException {
+ final int maxTries = 5;
+ for (int i = 1; i <= maxTries; i++) {
+ final String result = runCommand(command).trim();
+ if (result.equals(expectedResult)) return;
+ Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
+ + expectedResult + "' on attempt #; sleeping 1s before polling again");
+ Thread.sleep(1000);
+ }
+ fail("Command '" + command + "' did not return '" + expectedResult + "' after " + maxTries
+ + " attempts");
+ }
+
+ protected void addRestrictBackgroundWhitelist(int uid) throws Exception {
+ runCommand("cmd netpolicy add restrict-background-whitelist " + uid);
+ assertRestrictBackgroundWhitelist(uid, true);
+ }
+
+ private void addPowerSaveModeWhitelist(String packageName) throws Exception {
+ Log.i(TAG, "Adding package " + packageName + " to power-save-mode whitelist");
+ // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
+ // need to use netpolicy for whitelisting
+ runCommand("dumpsys deviceidle whitelist +" + packageName);
+ assertPowerSaveModeWhitelist(packageName, true);
+ }
+
+ protected boolean isDozeModeEnabled() throws Exception {
+ final String result = runCommand("cmd deviceidle enabled deep").trim();
+ return result.equals("1");
+ }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
new file mode 100644
index 0000000..49b5f9d
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2016 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 com.android.cts.net;
+
+public class HostsideVpnTests extends HostsideNetworkTestCase {
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ uninstallPackage(TEST_APP2_PKG, false);
+ installPackage(TEST_APP2_APK);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ uninstallPackage(TEST_APP2_PKG, true);
+ }
+
+ public void testDefault() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testDefault");
+ }
+
+ public void testAppAllowed() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testAppAllowed");
+ }
+
+ public void testAppDisallowed() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testAppDisallowed");
+ }
+
+ public void testGetConnectionOwnerUidSecurity() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testGetConnectionOwnerUidSecurity");
+ }
+
+ public void testSetProxy() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testSetProxy");
+ }
+
+ public void testSetProxyDisallowedApps() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testSetProxyDisallowedApps");
+ }
+
+ public void testNoProxy() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testNoProxy");
+ }
+
+ public void testBindToNetworkWithProxy() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testBindToNetworkWithProxy");
+ }
+
+ public void testVpnMeterednessWithNoUnderlyingNetwork() throws Exception {
+ runDeviceTests(
+ TEST_PKG, TEST_PKG + ".VpnTest", "testVpnMeterednessWithNoUnderlyingNetwork");
+ }
+
+ public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception {
+ runDeviceTests(
+ TEST_PKG, TEST_PKG + ".VpnTest", "testVpnMeterednessWithNullUnderlyingNetwork");
+ }
+
+ public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception {
+ runDeviceTests(
+ TEST_PKG, TEST_PKG + ".VpnTest", "testVpnMeterednessWithNonNullUnderlyingNetwork");
+ }
+
+ public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception {
+ runDeviceTests(
+ TEST_PKG, TEST_PKG + ".VpnTest", "testAlwaysMeteredVpnWithNullUnderlyingNetwork");
+ }
+
+ public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception {
+ runDeviceTests(
+ TEST_PKG,
+ TEST_PKG + ".VpnTest",
+ "testAlwaysMeteredVpnWithNonNullUnderlyingNetwork");
+ }
+
+ public void testB141603906() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testB141603906");
+ }
+
+ public void testDownloadWithDownloadManagerDisallowed() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest",
+ "testDownloadWithDownloadManagerDisallowed");
+ }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java b/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java
new file mode 100644
index 0000000..23aca24
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 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 com.android.cts.net;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil;
+import com.android.tradefed.targetprep.ITargetPreparer;
+
+public class NetworkPolicyTestsPreparer implements ITargetPreparer {
+ private ITestDevice mDevice;
+ private boolean mOriginalAirplaneModeEnabled;
+ private String mOriginalAppStandbyEnabled;
+ private String mOriginalBatteryStatsConstants;
+ private final static String KEY_STABLE_CHARGING_DELAY_MS = "battery_charged_delay_ms";
+ private final static int DESIRED_STABLE_CHARGING_DELAY_MS = 0;
+
+ @Override
+ public void setUp(TestInformation testInformation) throws DeviceNotAvailableException {
+ mDevice = testInformation.getDevice();
+ mOriginalAppStandbyEnabled = getAppStandbyEnabled();
+ setAppStandbyEnabled("1");
+ LogUtil.CLog.d("Original app_standby_enabled: " + mOriginalAppStandbyEnabled);
+
+ mOriginalBatteryStatsConstants = getBatteryStatsConstants();
+ setBatteryStatsConstants(
+ KEY_STABLE_CHARGING_DELAY_MS + "=" + DESIRED_STABLE_CHARGING_DELAY_MS);
+ LogUtil.CLog.d("Original battery_saver_constants: " + mOriginalBatteryStatsConstants);
+
+ mOriginalAirplaneModeEnabled = getAirplaneModeEnabled();
+ // Turn off airplane mode in case another test left the device in that state.
+ setAirplaneModeEnabled(false);
+ LogUtil.CLog.d("Original airplane mode state: " + mOriginalAirplaneModeEnabled);
+ }
+
+ @Override
+ public void tearDown(TestInformation testInformation, Throwable e)
+ throws DeviceNotAvailableException {
+ setAirplaneModeEnabled(mOriginalAirplaneModeEnabled);
+ setAppStandbyEnabled(mOriginalAppStandbyEnabled);
+ setBatteryStatsConstants(mOriginalBatteryStatsConstants);
+ }
+
+ private void setAirplaneModeEnabled(boolean enable) throws DeviceNotAvailableException {
+ executeCmd("cmd connectivity airplane-mode " + (enable ? "enable" : "disable"));
+ }
+
+ private boolean getAirplaneModeEnabled() throws DeviceNotAvailableException {
+ return "enabled".equals(executeCmd("cmd connectivity airplane-mode").trim());
+ }
+
+ private void setAppStandbyEnabled(String appStandbyEnabled) throws DeviceNotAvailableException {
+ if ("null".equals(appStandbyEnabled)) {
+ executeCmd("settings delete global app_standby_enabled");
+ } else {
+ executeCmd("settings put global app_standby_enabled " + appStandbyEnabled);
+ }
+ }
+
+ private String getAppStandbyEnabled() throws DeviceNotAvailableException {
+ return executeCmd("settings get global app_standby_enabled").trim();
+ }
+
+ private void setBatteryStatsConstants(String batteryStatsConstants)
+ throws DeviceNotAvailableException {
+ executeCmd("settings put global battery_stats_constants \"" + batteryStatsConstants + "\"");
+ }
+
+ private String getBatteryStatsConstants() throws DeviceNotAvailableException {
+ return executeCmd("settings get global battery_stats_constants");
+ }
+
+ private String executeCmd(String cmd) throws DeviceNotAvailableException {
+ final String output = mDevice.executeShellCommand(cmd).trim();
+ LogUtil.CLog.d("Output for '%s': %s", cmd, output);
+ return output;
+ }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
new file mode 100644
index 0000000..19e61c6
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2018 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.security.cts;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+import com.android.tradefed.testtype.IDeviceTest;
+
+import java.lang.Integer;
+import java.lang.String;
+import java.util.Arrays;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Host-side tests for values in /proc/net.
+ *
+ * These tests analyze /proc/net to verify that certain networking properties are correct.
+ */
+public class ProcNetTest extends DeviceTestCase implements IBuildReceiver, IDeviceTest {
+ private static final String SPI_TIMEOUT_SYSCTL = "/proc/sys/net/core/xfrm_acq_expires";
+ private static final int MIN_ACQ_EXPIRES = 3600;
+ // Global sysctls. Must be present and set to 1.
+ private static final String[] GLOBAL_SYSCTLS = {
+ "/proc/sys/net/ipv4/fwmark_reflect",
+ "/proc/sys/net/ipv6/fwmark_reflect",
+ "/proc/sys/net/ipv4/tcp_fwmark_accept",
+ };
+
+ // Per-interface IPv6 autoconf sysctls.
+ private static final String IPV6_SYSCTL_DIR = "/proc/sys/net/ipv6/conf";
+ private static final String AUTOCONF_SYSCTL = "accept_ra_rt_table";
+
+ // Expected values for MIN|MAX_PLEN.
+ private static final String ACCEPT_RA_RT_INFO_MIN_PLEN_STRING = "accept_ra_rt_info_min_plen";
+ private static final int ACCEPT_RA_RT_INFO_MIN_PLEN_VALUE = 48;
+ private static final String ACCEPT_RA_RT_INFO_MAX_PLEN_STRING = "accept_ra_rt_info_max_plen";
+ private static final int ACCEPT_RA_RT_INFO_MAX_PLEN_VALUE = 64;
+ // Expected values for RFC 7559 router soliciations.
+ // Maximum number of router solicitations to send. -1 means no limit.
+ private static final int IPV6_WIFI_ROUTER_SOLICITATIONS = -1;
+ private ITestDevice mDevice;
+ private IBuildInfo mBuild;
+ private String[] mSysctlDirs;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setBuild(IBuildInfo build) {
+ mBuild = build;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setDevice(ITestDevice device) {
+ super.setDevice(device);
+ mDevice = device;
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mSysctlDirs = getSysctlDirs();
+ }
+
+ private String[] getSysctlDirs() throws Exception {
+ String interfaceDirs[] = mDevice.executeAdbCommand("shell", "ls", "-1",
+ IPV6_SYSCTL_DIR).split("\n");
+ List<String> interfaceDirsList = new ArrayList<String>(Arrays.asList(interfaceDirs));
+ interfaceDirsList.remove("all");
+ interfaceDirsList.remove("lo");
+ return interfaceDirsList.toArray(new String[interfaceDirsList.size()]);
+ }
+
+
+ protected void assertLess(String sysctl, int a, int b) {
+ assertTrue("value of " + sysctl + ": expected < " + b + " but was: " + a, a < b);
+ }
+
+ protected void assertAtLeast(String sysctl, int a, int b) {
+ assertTrue("value of " + sysctl + ": expected >= " + b + " but was: " + a, a >= b);
+ }
+
+ public int readIntFromPath(String path) throws Exception {
+ String mode = mDevice.executeAdbCommand("shell", "stat", "-c", "%a", path).trim();
+ String user = mDevice.executeAdbCommand("shell", "stat", "-c", "%u", path).trim();
+ String group = mDevice.executeAdbCommand("shell", "stat", "-c", "%g", path).trim();
+ assertEquals(mode, "644");
+ assertEquals(user, "0");
+ assertEquals(group, "0");
+ return Integer.parseInt(mDevice.executeAdbCommand("shell", "cat", path).trim());
+ }
+
+ /**
+ * Checks that SPI default timeouts are overridden, and set to a reasonable length of time
+ */
+ public void testMinAcqExpires() throws Exception {
+ int value = readIntFromPath(SPI_TIMEOUT_SYSCTL);
+ assertAtLeast(SPI_TIMEOUT_SYSCTL, value, MIN_ACQ_EXPIRES);
+ }
+
+ /**
+ * Checks that the sysctls for multinetwork kernel features are present and
+ * enabled.
+ */
+ public void testProcSysctls() throws Exception {
+ for (String sysctl : GLOBAL_SYSCTLS) {
+ int value = readIntFromPath(sysctl);
+ assertEquals(sysctl, 1, value);
+ }
+
+ for (String interfaceDir : mSysctlDirs) {
+ String path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + AUTOCONF_SYSCTL;
+ int value = readIntFromPath(path);
+ assertLess(path, value, 0);
+ }
+ }
+
+ /**
+ * Verify that accept_ra_rt_info_{min,max}_plen exists and is set to the expected value
+ */
+ public void testAcceptRaRtInfoMinMaxPlen() throws Exception {
+ for (String interfaceDir : mSysctlDirs) {
+ String path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "accept_ra_rt_info_min_plen";
+ int value = readIntFromPath(path);
+ assertEquals(path, value, ACCEPT_RA_RT_INFO_MIN_PLEN_VALUE);
+ path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "accept_ra_rt_info_max_plen";
+ value = readIntFromPath(path);
+ assertEquals(path, value, ACCEPT_RA_RT_INFO_MAX_PLEN_VALUE);
+ }
+ }
+
+ /**
+ * Verify that router_solicitations exists and is set to the expected value
+ * and verify that router_solicitation_max_interval exists and is in an acceptable interval.
+ */
+ public void testRouterSolicitations() throws Exception {
+ for (String interfaceDir : mSysctlDirs) {
+ String path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "router_solicitations";
+ int value = readIntFromPath(path);
+ assertEquals(IPV6_WIFI_ROUTER_SOLICITATIONS, value);
+ path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "router_solicitation_max_interval";
+ int interval = readIntFromPath(path);
+ final int lowerBoundSec = 15 * 60;
+ final int upperBoundSec = 60 * 60;
+ assertTrue(lowerBoundSec <= interval);
+ assertTrue(interval <= upperBoundSec);
+ }
+ }
+}
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
new file mode 100644
index 0000000..528171a
--- /dev/null
+++ b/tests/cts/net/Android.bp
@@ -0,0 +1,84 @@
+// Copyright (C) 2008 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.
+
+java_defaults {
+ name: "CtsNetTestCasesDefaults",
+ defaults: ["cts_defaults"],
+
+ // Include both the 32 and 64 bit versions
+ compile_multilib: "both",
+
+ libs: [
+ "voip-common",
+ "android.test.base",
+ ],
+
+ jni_libs: [
+ "libcts_jni",
+ "libnativedns_jni",
+ "libnativemultinetwork_jni",
+ "libnativehelper_compat_libc++",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ jarjar_rules: "jarjar-rules-shared.txt",
+ static_libs: [
+ "FrameworksNetCommonTests",
+ "TestNetworkStackLib",
+ "core-tests-support",
+ "cts-net-utils",
+ "ctstestrunner-axt",
+ "junit",
+ "junit-params",
+ "net-utils-framework-common",
+ "truth-prebuilt",
+ ],
+
+ // uncomment when b/13249961 is fixed
+ // sdk_version: "current",
+ platform_apis: true,
+}
+
+// Networking CTS tests for development and release. These tests always target the platform SDK
+// version, and are subject to all the restrictions appropriate to that version. Before SDK
+// finalization, these tests have a min_sdk_version of 10000, and cannot be installed on release
+// devices.
+android_test {
+ name: "CtsNetTestCases",
+ defaults: ["CtsNetTestCasesDefaults"],
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+ test_config_template: "AndroidTestTemplate.xml",
+}
+
+// Networking CTS tests that target the latest released SDK. These tests can be installed on release
+// devices at any point in the Android release cycle and are useful for qualifying mainline modules
+// on release devices.
+android_test {
+ name: "CtsNetTestCasesLatestSdk",
+ defaults: ["CtsNetTestCasesDefaults"],
+ jni_uses_sdk_apis: true,
+ min_sdk_version: "29",
+ target_sdk_version: "30",
+ test_suites: [
+ "general-tests",
+ "mts",
+ ],
+ test_config_template: "AndroidTestTemplate.xml",
+}
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
new file mode 100644
index 0000000..a7e2bd7
--- /dev/null
+++ b/tests/cts/net/AndroidManifest.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2007 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.net.cts"
+ android:targetSandboxVersion="2">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+ <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" />
+ <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
+
+ <!-- This test also uses signature permissions through adopting the shell identity.
+ The permissions acquired that way include (probably not exhaustive) :
+ android.permission.MANAGE_TEST_NETWORKS
+ -->
+
+ <application android:usesCleartextTraffic="true">
+ <uses-library android:name="android.test.runner" />
+ <uses-library android:name="org.apache.http.legacy" android:required="false" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.net.cts"
+ android:label="CTS tests of android.net">
+ <meta-data android:name="listener"
+ android:value="com.android.cts.runner.CtsTestRunListener" />
+ </instrumentation>
+
+</manifest>
+
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
new file mode 100644
index 0000000..78a01e2
--- /dev/null
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -0,0 +1,35 @@
+<!-- Copyright (C) 2015 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.
+-->
+<configuration description="Test config for {MODULE}">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="networking" />
+ <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
+ <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+ <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
+ <option name="not-shardable" value="true" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="{MODULE}.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.net.cts" />
+ <option name="runtime-hint" value="9m4s" />
+ <option name="hidden-api-checks" value="false" />
+ <option name="isolated-storage" value="false" />
+ </test>
+</configuration>
diff --git a/tests/cts/net/OWNERS b/tests/cts/net/OWNERS
new file mode 100644
index 0000000..d558556
--- /dev/null
+++ b/tests/cts/net/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 31808
+lorenzo@google.com
+satk@google.com
diff --git a/tests/cts/net/TEST_MAPPING b/tests/cts/net/TEST_MAPPING
new file mode 100644
index 0000000..7545cb0
--- /dev/null
+++ b/tests/cts/net/TEST_MAPPING
@@ -0,0 +1,23 @@
+{
+ // TODO: move to mainline-presubmit once supported
+ "presubmit": [
+ {
+ "name": "CtsNetTestCasesLatestSdk",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ }
+ ]
+ }
+ ],
+ "mainline-presubmit": [
+ {
+ "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ }
+ ]
+ }
+ ]
+}
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
new file mode 100644
index 0000000..e43a5e8
--- /dev/null
+++ b/tests/cts/net/api23Test/Android.bp
@@ -0,0 +1,51 @@
+// Copyright (C) 2019 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.
+
+android_test {
+ name: "CtsNetApi23TestCases",
+ defaults: ["cts_defaults"],
+
+ // Include both the 32 and 64 bit versions
+ compile_multilib: "both",
+
+ libs: [
+ "android.test.base",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+
+ static_libs: [
+ "core-tests-support",
+ "compatibility-device-util-axt",
+ "cts-net-utils",
+ "ctstestrunner-axt",
+ "ctstestserver",
+ "mockwebserver",
+ "junit",
+ "junit-params",
+ "truth-prebuilt",
+ ],
+
+ platform_apis: true,
+
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+
+}
diff --git a/tests/cts/net/api23Test/AndroidManifest.xml b/tests/cts/net/api23Test/AndroidManifest.xml
new file mode 100644
index 0000000..4889660
--- /dev/null
+++ b/tests/cts/net/api23Test/AndroidManifest.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2019 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.net.cts.api23test">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application android:usesCleartextTraffic="true">
+ <uses-library android:name="android.test.runner" />
+
+ <receiver android:name=".ConnectivityReceiver">
+ <intent-filter>
+ <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
+ </intent-filter>
+ </receiver>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.net.cts.api23test"
+ android:label="CTS tests of android.net">
+ <meta-data android:name="listener"
+ android:value="com.android.cts.runner.CtsTestRunListener" />
+ </instrumentation>
+</manifest>
+
diff --git a/tests/cts/net/api23Test/AndroidTest.xml b/tests/cts/net/api23Test/AndroidTest.xml
new file mode 100644
index 0000000..8042d50
--- /dev/null
+++ b/tests/cts/net/api23Test/AndroidTest.xml
@@ -0,0 +1,31 @@
+<!-- Copyright (C) 2019 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.
+-->
+<configuration description="Config for CTS Net API23 test cases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="networking" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <option name="not-shardable" value="true" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="CtsNetApi23TestCases.apk" />
+ <option name="test-file-name" value="CtsNetTestAppForApi23.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.net.cts.api23test" />
+ <option name="hidden-api-checks" value="false" />
+ </test>
+</configuration>
diff --git a/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java
new file mode 100644
index 0000000..cdb66e3
--- /dev/null
+++ b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2019 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.api23test;
+
+import static android.content.pm.PackageManager.FEATURE_WIFI;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.cts.util.CtsNetUtils;
+import android.os.Looper;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class ConnectivityManagerApi23Test extends AndroidTestCase {
+ private static final String TAG = ConnectivityManagerApi23Test.class.getSimpleName();
+ private static final int SEND_BROADCAST_TIMEOUT = 30000;
+ // Intent string to get the number of wifi CONNECTIVITY_ACTION callbacks the test app has seen
+ public static final String GET_WIFI_CONNECTIVITY_ACTION_COUNT =
+ "android.net.cts.appForApi23.getWifiConnectivityActionCount";
+ // Action sent to ConnectivityActionReceiver when a network callback is sent via PendingIntent.
+
+ private Context mContext;
+ private PackageManager mPackageManager;
+ private CtsNetUtils mCtsNetUtils;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ Looper.prepare();
+ mContext = getContext();
+ mPackageManager = mContext.getPackageManager();
+ mCtsNetUtils = new CtsNetUtils(mContext);
+ }
+
+ /**
+ * Tests reporting of connectivity changed.
+ */
+ public void testConnectivityChanged_manifestRequestOnly_shouldNotReceiveIntent() {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testConnectivityChanged_manifestRequestOnly_shouldNotReceiveIntent cannot execute unless device supports WiFi");
+ return;
+ }
+ ConnectivityReceiver.prepare();
+
+ mCtsNetUtils.toggleWifi();
+
+ // The connectivity broadcast has been sent; push through a terminal broadcast
+ // to wait for in the receive to confirm it didn't see the connectivity change.
+ Intent finalIntent = new Intent(ConnectivityReceiver.FINAL_ACTION);
+ finalIntent.setClass(mContext, ConnectivityReceiver.class);
+ mContext.sendBroadcast(finalIntent);
+ assertFalse(ConnectivityReceiver.waitForBroadcast());
+ }
+
+ public void testConnectivityChanged_manifestRequestOnlyPreN_shouldReceiveIntent()
+ throws InterruptedException {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testConnectivityChanged_manifestRequestOnlyPreN_shouldReceiveIntent cannot"
+ + "execute unless device supports WiFi");
+ return;
+ }
+ mContext.startActivity(new Intent()
+ .setComponent(new ComponentName("android.net.cts.appForApi23",
+ "android.net.cts.appForApi23.ConnectivityListeningActivity"))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ Thread.sleep(200);
+
+ mCtsNetUtils.toggleWifi();
+
+ Intent getConnectivityCount = new Intent(GET_WIFI_CONNECTIVITY_ACTION_COUNT);
+ assertEquals(2, sendOrderedBroadcastAndReturnResultCode(
+ getConnectivityCount, SEND_BROADCAST_TIMEOUT));
+ }
+
+ public void testConnectivityChanged_whenRegistered_shouldReceiveIntent() {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testConnectivityChanged_whenRegistered_shouldReceiveIntent cannot execute unless device supports WiFi");
+ return;
+ }
+ ConnectivityReceiver.prepare();
+ ConnectivityReceiver receiver = new ConnectivityReceiver();
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ mContext.registerReceiver(receiver, filter);
+
+ mCtsNetUtils.toggleWifi();
+ Intent finalIntent = new Intent(ConnectivityReceiver.FINAL_ACTION);
+ finalIntent.setClass(mContext, ConnectivityReceiver.class);
+ mContext.sendBroadcast(finalIntent);
+
+ assertTrue(ConnectivityReceiver.waitForBroadcast());
+ }
+
+ private int sendOrderedBroadcastAndReturnResultCode(
+ Intent intent, int timeoutMs) throws InterruptedException {
+ final LinkedBlockingQueue<Integer> result = new LinkedBlockingQueue<>(1);
+ mContext.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ result.offer(getResultCode());
+ }
+ }, null, 0, null, null);
+
+ Integer resultCode = result.poll(timeoutMs, TimeUnit.MILLISECONDS);
+ assertNotNull("Timed out (more than " + timeoutMs +
+ " milliseconds) waiting for result code for broadcast", resultCode);
+ return resultCode;
+ }
+
+}
\ No newline at end of file
diff --git a/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityReceiver.java b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityReceiver.java
new file mode 100644
index 0000000..9d2b8ad
--- /dev/null
+++ b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityReceiver.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2016 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.api23test;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class ConnectivityReceiver extends BroadcastReceiver {
+ static boolean sReceivedConnectivity;
+ static boolean sReceivedFinal;
+ static CountDownLatch sLatch;
+
+ static void prepare() {
+ synchronized (ConnectivityReceiver.class) {
+ sReceivedConnectivity = sReceivedFinal = false;
+ sLatch = new CountDownLatch(1);
+ }
+ }
+
+ static boolean waitForBroadcast() {
+ try {
+ sLatch.await(30, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+ synchronized (ConnectivityReceiver.class) {
+ sLatch = null;
+ if (!sReceivedFinal) {
+ throw new IllegalStateException("Never received final broadcast");
+ }
+ return sReceivedConnectivity;
+ }
+ }
+
+ static final String FINAL_ACTION = "android.net.cts.action.FINAL";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.i("ConnectivityReceiver", "Received: " + intent.getAction());
+ if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
+ sReceivedConnectivity = true;
+ } else if (FINAL_ACTION.equals(intent.getAction())) {
+ sReceivedFinal = true;
+ if (sLatch != null) {
+ sLatch.countDown();
+ }
+ }
+ }
+}
diff --git a/tests/cts/net/appForApi23/Android.bp b/tests/cts/net/appForApi23/Android.bp
new file mode 100644
index 0000000..cec6d7f
--- /dev/null
+++ b/tests/cts/net/appForApi23/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2016 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.
+
+android_test {
+ name: "CtsNetTestAppForApi23",
+ defaults: ["cts_defaults"],
+
+ // Include both the 32 and 64 bit versions
+ compile_multilib: "both",
+
+ srcs: ["src/**/*.java"],
+
+ sdk_version: "23",
+
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+
+}
diff --git a/tests/cts/net/appForApi23/AndroidManifest.xml b/tests/cts/net/appForApi23/AndroidManifest.xml
new file mode 100644
index 0000000..ed4cedb
--- /dev/null
+++ b/tests/cts/net/appForApi23/AndroidManifest.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2016 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.net.cts.appForApi23">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application>
+ <receiver android:name=".ConnectivityReceiver">
+ <intent-filter>
+ <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.net.cts.appForApi23.getWifiConnectivityActionCount" />
+ </intent-filter>
+ </receiver>
+
+ <activity android:name=".ConnectivityListeningActivity"
+ android:label="ConnectivityListeningActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ </application>
+
+</manifest>
+
diff --git a/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityListeningActivity.java b/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityListeningActivity.java
new file mode 100644
index 0000000..24fb68e8
--- /dev/null
+++ b/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityListeningActivity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2016 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.appForApi23;
+
+import android.app.Activity;
+
+// Stub activity used to start the app
+public class ConnectivityListeningActivity extends Activity {
+}
\ No newline at end of file
diff --git a/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityReceiver.java b/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityReceiver.java
new file mode 100644
index 0000000..8039a4f
--- /dev/null
+++ b/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityReceiver.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016 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.appForApi23;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+
+public class ConnectivityReceiver extends BroadcastReceiver {
+ public static String GET_WIFI_CONNECTIVITY_ACTION_COUNT =
+ "android.net.cts.appForApi23.getWifiConnectivityActionCount";
+
+ private static int sWifiConnectivityActionCount = 0;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
+ int networkType = intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, 0);
+ if (networkType == ConnectivityManager.TYPE_WIFI) {
+ sWifiConnectivityActionCount++;
+ }
+ }
+ if (GET_WIFI_CONNECTIVITY_ACTION_COUNT.equals(intent.getAction())) {
+ setResultCode(sWifiConnectivityActionCount);
+ }
+ }
+}
diff --git a/tests/cts/net/assets/network_watchlist_config_empty_for_test.xml b/tests/cts/net/assets/network_watchlist_config_empty_for_test.xml
new file mode 100644
index 0000000..19628d1
--- /dev/null
+++ b/tests/cts/net/assets/network_watchlist_config_empty_for_test.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright (C) 2018 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.
+*/
+-->
+<!-- This test config file is for NetworkWatchlistTest tests -->
+<watchlist-config>
+ <sha256-domain>
+ </sha256-domain>
+ <sha256-ip>
+ </sha256-ip>
+ <crc32-domain>
+ </crc32-domain>
+ <crc32-ip>
+ </crc32-ip>
+</watchlist-config>
diff --git a/tests/cts/net/assets/network_watchlist_config_for_test.xml b/tests/cts/net/assets/network_watchlist_config_for_test.xml
new file mode 100644
index 0000000..835ae0f
--- /dev/null
+++ b/tests/cts/net/assets/network_watchlist_config_for_test.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright (C) 2018 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.
+*/
+-->
+<!-- This test config file just contains some random hashes for testing
+ConnectivityManager.getWatchlistConfigHash() -->
+<watchlist-config>
+ <sha256-domain>
+ <hash>F0905DA7549614957B449034C281EF7BDEFDBC2B6E050AD1E78D6DE18FBD0D5F</hash>
+ </sha256-domain>
+ <sha256-ip>
+ <hash>18DD41C9F2E8E4879A1575FB780514EF33CF6E1F66578C4AE7CCA31F49B9F2EC</hash>
+ </sha256-ip>
+ <crc32-domain>
+ <hash>AAAAAAAA</hash>
+ </crc32-domain>
+ <crc32-ip>
+ <hash>BBBBBBBB</hash>
+ </crc32-ip>
+</watchlist-config>
diff --git a/tests/cts/net/jarjar-rules-shared.txt b/tests/cts/net/jarjar-rules-shared.txt
new file mode 100644
index 0000000..11dba74
--- /dev/null
+++ b/tests/cts/net/jarjar-rules-shared.txt
@@ -0,0 +1,2 @@
+# Module library in frameworks/libs/net
+rule com.android.net.module.util.** android.net.cts.util.@1
\ No newline at end of file
diff --git a/tests/cts/net/jni/Android.bp b/tests/cts/net/jni/Android.bp
new file mode 100644
index 0000000..3953aeb
--- /dev/null
+++ b/tests/cts/net/jni/Android.bp
@@ -0,0 +1,51 @@
+// Copyright (C) 2013 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.
+
+cc_library_shared {
+ name: "libnativedns_jni",
+
+ srcs: ["NativeDnsJni.c"],
+ sdk_version: "current",
+
+ shared_libs: [
+ "libnativehelper_compat_libc++",
+ "liblog",
+ ],
+ stl: "libc++_static",
+
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ ],
+
+}
+
+cc_library_shared {
+ name: "libnativemultinetwork_jni",
+
+ srcs: ["NativeMultinetworkJni.cpp"],
+ sdk_version: "current",
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-format",
+ ],
+ shared_libs: [
+ "libandroid",
+ "libnativehelper_compat_libc++",
+ "liblog",
+ ],
+ stl: "libc++_static",
+}
diff --git a/tests/cts/net/jni/NativeDnsJni.c b/tests/cts/net/jni/NativeDnsJni.c
new file mode 100644
index 0000000..4ec800e
--- /dev/null
+++ b/tests/cts/net/jni/NativeDnsJni.c
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+#include <arpa/inet.h>
+#include <jni.h>
+#include <netdb.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <android/log.h>
+
+#define LOG_TAG "NativeDns-JNI"
+#define LOGD(fmt, ...) \
+ __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##__VA_ARGS__)
+
+const char *GoogleDNSIpV4Address="8.8.8.8";
+const char *GoogleDNSIpV4Address2="8.8.4.4";
+const char *GoogleDNSIpV6Address="2001:4860:4860::8888";
+const char *GoogleDNSIpV6Address2="2001:4860:4860::8844";
+
+JNIEXPORT jboolean Java_android_net_cts_DnsTest_testNativeDns(JNIEnv* env, jclass class)
+{
+ const char *node = "www.google.com";
+ char *service = NULL;
+ struct addrinfo *answer;
+
+ int res = getaddrinfo(node, service, NULL, &answer);
+ LOGD("getaddrinfo(www.google.com) gave res=%d (%s)", res, gai_strerror(res));
+ if (res != 0) return JNI_FALSE;
+
+ // check for v4 & v6
+ {
+ int foundv4 = 0;
+ int foundv6 = 0;
+ struct addrinfo *current = answer;
+ while (current != NULL) {
+ char buf[256];
+ if (current->ai_addr->sa_family == AF_INET) {
+ inet_ntop(current->ai_family, &((struct sockaddr_in *)current->ai_addr)->sin_addr,
+ buf, sizeof(buf));
+ foundv4 = 1;
+ LOGD(" %s", buf);
+ } else if (current->ai_addr->sa_family == AF_INET6) {
+ inet_ntop(current->ai_family, &((struct sockaddr_in6 *)current->ai_addr)->sin6_addr,
+ buf, sizeof(buf));
+ foundv6 = 1;
+ LOGD(" %s", buf);
+ }
+ current = current->ai_next;
+ }
+
+ freeaddrinfo(answer);
+ answer = NULL;
+ if (foundv4 != 1 && foundv6 != 1) {
+ LOGD("getaddrinfo(www.google.com) didn't find either v4 or v6 address");
+ return JNI_FALSE;
+ }
+ }
+
+ node = "ipv6.google.com";
+ res = getaddrinfo(node, service, NULL, &answer);
+ LOGD("getaddrinfo(ipv6.google.com) gave res=%d", res);
+ if (res != 0) return JNI_FALSE;
+
+ {
+ int foundv4 = 0;
+ int foundv6 = 0;
+ struct addrinfo *current = answer;
+ while (current != NULL) {
+ char buf[256];
+ if (current->ai_addr->sa_family == AF_INET) {
+ inet_ntop(current->ai_family, &((struct sockaddr_in *)current->ai_addr)->sin_addr,
+ buf, sizeof(buf));
+ LOGD(" %s", buf);
+ foundv4 = 1;
+ } else if (current->ai_addr->sa_family == AF_INET6) {
+ inet_ntop(current->ai_family, &((struct sockaddr_in6 *)current->ai_addr)->sin6_addr,
+ buf, sizeof(buf));
+ LOGD(" %s", buf);
+ foundv6 = 1;
+ }
+ current = current->ai_next;
+ }
+
+ freeaddrinfo(answer);
+ answer = NULL;
+ if (foundv4 == 1 || foundv6 != 1) {
+ LOGD("getaddrinfo(ipv6.google.com) didn't find only v6");
+ return JNI_FALSE;
+ }
+ }
+
+ // getnameinfo
+ struct sockaddr_in sa4;
+ sa4.sin_family = AF_INET;
+ sa4.sin_port = 0;
+ inet_pton(AF_INET, GoogleDNSIpV4Address, &(sa4.sin_addr));
+
+ struct sockaddr_in6 sa6;
+ sa6.sin6_family = AF_INET6;
+ sa6.sin6_port = 0;
+ sa6.sin6_flowinfo = 0;
+ sa6.sin6_scope_id = 0;
+ inet_pton(AF_INET6, GoogleDNSIpV6Address2, &(sa6.sin6_addr));
+
+ char buf[NI_MAXHOST];
+ int flags = NI_NAMEREQD;
+
+ res = getnameinfo((const struct sockaddr*)&sa4, sizeof(sa4), buf, sizeof(buf), NULL, 0, flags);
+ if (res != 0) {
+ LOGD("getnameinfo(%s (GoogleDNS) ) gave error %d (%s)", GoogleDNSIpV4Address, res,
+ gai_strerror(res));
+ return JNI_FALSE;
+ }
+ if (strstr(buf, "google.com") == NULL && strstr(buf, "dns.google") == NULL) {
+ LOGD("getnameinfo(%s (GoogleDNS) ) didn't return google.com or dns.google: %s",
+ GoogleDNSIpV4Address, buf);
+ return JNI_FALSE;
+ }
+
+ memset(buf, 0, sizeof(buf));
+ res = getnameinfo((const struct sockaddr*)&sa6, sizeof(sa6), buf, sizeof(buf), NULL, 0, flags);
+ if (res != 0) {
+ LOGD("getnameinfo(%s (GoogleDNS) ) gave error %d (%s)", GoogleDNSIpV6Address2,
+ res, gai_strerror(res));
+ return JNI_FALSE;
+ }
+ if (strstr(buf, "google.com") == NULL && strstr(buf, "dns.google") == NULL) {
+ LOGD("getnameinfo(%s (GoogleDNS) ) didn't return google.com or dns.google: %s",
+ GoogleDNSIpV6Address2, buf);
+ return JNI_FALSE;
+ }
+
+ // gethostbyname
+ struct hostent *my_hostent = gethostbyname("www.youtube.com");
+ if (my_hostent == NULL) {
+ LOGD("gethostbyname(www.youtube.com) gave null response");
+ return JNI_FALSE;
+ }
+ if ((my_hostent->h_addr_list == NULL) || (*my_hostent->h_addr_list == NULL)) {
+ LOGD("gethostbyname(www.youtube.com) gave 0 addresses");
+ return JNI_FALSE;
+ }
+ {
+ char **current = my_hostent->h_addr_list;
+ while (*current != NULL) {
+ char buf[256];
+ inet_ntop(my_hostent->h_addrtype, *current, buf, sizeof(buf));
+ LOGD("gethostbyname(www.youtube.com) gave %s", buf);
+ current++;
+ }
+ }
+
+ // gethostbyaddr
+ char addr6[16];
+ inet_pton(AF_INET6, GoogleDNSIpV6Address, addr6);
+ my_hostent = gethostbyaddr(addr6, sizeof(addr6), AF_INET6);
+ if (my_hostent == NULL) {
+ LOGD("gethostbyaddr(%s (GoogleDNS) ) gave null response", GoogleDNSIpV6Address);
+ return JNI_FALSE;
+ }
+
+ LOGD("gethostbyaddr(%s (GoogleDNS) ) gave %s for name", GoogleDNSIpV6Address,
+ my_hostent->h_name ? my_hostent->h_name : "null");
+
+ if (my_hostent->h_name == NULL) return JNI_FALSE;
+ return JNI_TRUE;
+}
diff --git a/tests/cts/net/jni/NativeMultinetworkJni.cpp b/tests/cts/net/jni/NativeMultinetworkJni.cpp
new file mode 100644
index 0000000..60e31bc
--- /dev/null
+++ b/tests/cts/net/jni/NativeMultinetworkJni.cpp
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+
+#define LOG_TAG "MultinetworkApiTest"
+
+#include <arpa/inet.h>
+#include <arpa/nameser.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <jni.h>
+#include <netdb.h>
+#include <poll.h> /* poll */
+#include <resolv.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+
+#include <string>
+
+#include <android/log.h>
+#include <android/multinetwork.h>
+#include <nativehelper/JNIHelp.h>
+
+#define LOGD(fmt, ...) \
+ __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##__VA_ARGS__)
+
+#define EXPECT_GE(env, actual, expected, msg) \
+ do { \
+ if (actual < expected) { \
+ jniThrowExceptionFmt(env, "java/lang/AssertionError", \
+ "%s:%d: %s EXPECT_GE: expected %d, got %d", \
+ __FILE__, __LINE__, msg, expected, actual); \
+ } \
+ } while (0)
+
+#define EXPECT_GT(env, actual, expected, msg) \
+ do { \
+ if (actual <= expected) { \
+ jniThrowExceptionFmt(env, "java/lang/AssertionError", \
+ "%s:%d: %s EXPECT_GT: expected %d, got %d", \
+ __FILE__, __LINE__, msg, expected, actual); \
+ } \
+ } while (0)
+
+#define EXPECT_EQ(env, expected, actual, msg) \
+ do { \
+ if (actual != expected) { \
+ jniThrowExceptionFmt(env, "java/lang/AssertionError", \
+ "%s:%d: %s EXPECT_EQ: expected %d, got %d", \
+ __FILE__, __LINE__, msg, expected, actual); \
+ } \
+ } while (0)
+
+static const int MAXPACKET = 8 * 1024;
+static const int TIMEOUT_MS = 15000;
+static const char kHostname[] = "connectivitycheck.android.com";
+static const char kNxDomainName[] = "test1-nx.metric.gstatic.com";
+static const char kGoogleName[] = "www.google.com";
+
+int makeQuery(const char* name, int qtype, uint8_t* buf, size_t buflen) {
+ return res_mkquery(ns_o_query, name, ns_c_in, qtype, NULL, 0, NULL, buf, buflen);
+}
+
+int getAsyncResponse(JNIEnv* env, int fd, int timeoutMs, int* rcode, uint8_t* buf, size_t bufLen) {
+ struct pollfd wait_fd = { .fd = fd, .events = POLLIN };
+
+ poll(&wait_fd, 1, timeoutMs);
+ if (wait_fd.revents & POLLIN) {
+ int n = android_res_nresult(fd, rcode, buf, bufLen);
+ // Verify that android_res_nresult() closed the fd
+ char dummy;
+ EXPECT_EQ(env, -1, read(fd, &dummy, sizeof(dummy)), "res_nresult check for closing fd");
+ EXPECT_EQ(env, EBADF, errno, "res_nresult check for errno");
+ return n;
+ }
+
+ return -ETIMEDOUT;
+}
+
+int extractIpAddressAnswers(uint8_t* buf, size_t bufLen, int family) {
+ ns_msg handle;
+ if (ns_initparse((const uint8_t*) buf, bufLen, &handle) < 0) {
+ return -errno;
+ }
+ const int ancount = ns_msg_count(handle, ns_s_an);
+ // Answer count = 0 is valid(e.g. response of query with root)
+ if (!ancount) {
+ return 0;
+ }
+ ns_rr rr;
+ bool hasValidAns = false;
+ for (int i = 0; i < ancount; i++) {
+ if (ns_parserr(&handle, ns_s_an, i, &rr) < 0) {
+ // If there is no valid answer, test will fail.
+ continue;
+ }
+ const uint8_t* rdata = ns_rr_rdata(rr);
+ char buffer[INET6_ADDRSTRLEN];
+ if (inet_ntop(family, (const char*) rdata, buffer, sizeof(buffer)) == NULL) {
+ return -errno;
+ }
+ hasValidAns = true;
+ }
+ return hasValidAns ? 0 : -EBADMSG;
+}
+
+int expectAnswersValid(JNIEnv* env, int fd, int family, int expectedRcode) {
+ int rcode = -1;
+ uint8_t buf[MAXPACKET] = {};
+ int res = getAsyncResponse(env, fd, TIMEOUT_MS, &rcode, buf, MAXPACKET);
+ if (res < 0) {
+ return res;
+ }
+
+ EXPECT_EQ(env, expectedRcode, rcode, "rcode is not expected");
+
+ if (expectedRcode == ns_r_noerror && res > 0) {
+ return extractIpAddressAnswers(buf, res, family);
+ }
+ return 0;
+}
+
+int expectAnswersNotValid(JNIEnv* env, int fd, int expectedErrno) {
+ int rcode = -1;
+ uint8_t buf[MAXPACKET] = {};
+ int res = getAsyncResponse(env, fd, TIMEOUT_MS, &rcode, buf, MAXPACKET);
+ if (res != expectedErrno) {
+ LOGD("res:%d, expectedErrno = %d", res, expectedErrno);
+ return (res > 0) ? -EREMOTEIO : res;
+ }
+ return 0;
+}
+
+extern "C"
+JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNqueryCheck(
+ JNIEnv* env, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+
+ // V4
+ int fd = android_res_nquery(handle, kHostname, ns_c_in, ns_t_a, 0);
+ EXPECT_GE(env, fd, 0, "v4 res_nquery");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_noerror),
+ "v4 res_nquery check answers");
+
+ // V6
+ fd = android_res_nquery(handle, kHostname, ns_c_in, ns_t_aaaa, 0);
+ EXPECT_GE(env, fd, 0, "v6 res_nquery");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_noerror),
+ "v6 res_nquery check answers");
+}
+
+extern "C"
+JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNsendCheck(
+ JNIEnv* env, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+ // V4
+ uint8_t buf1[MAXPACKET] = {};
+
+ int len1 = makeQuery(kGoogleName, ns_t_a, buf1, sizeof(buf1));
+ EXPECT_GT(env, len1, 0, "v4 res_mkquery 1st");
+
+ uint8_t buf2[MAXPACKET] = {};
+ int len2 = makeQuery(kHostname, ns_t_a, buf2, sizeof(buf2));
+ EXPECT_GT(env, len2, 0, "v4 res_mkquery 2nd");
+
+ int fd1 = android_res_nsend(handle, buf1, len1, 0);
+ EXPECT_GE(env, fd1, 0, "v4 res_nsend 1st");
+ int fd2 = android_res_nsend(handle, buf2, len2, 0);
+ EXPECT_GE(env, fd2, 0, "v4 res_nsend 2nd");
+
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd2, AF_INET, ns_r_noerror),
+ "v4 res_nsend 2nd check answers");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd1, AF_INET, ns_r_noerror),
+ "v4 res_nsend 1st check answers");
+
+ // V6
+ memset(buf1, 0, sizeof(buf1));
+ memset(buf2, 0, sizeof(buf2));
+ len1 = makeQuery(kGoogleName, ns_t_aaaa, buf1, sizeof(buf1));
+ EXPECT_GT(env, len1, 0, "v6 res_mkquery 1st");
+ len2 = makeQuery(kHostname, ns_t_aaaa, buf2, sizeof(buf2));
+ EXPECT_GT(env, len2, 0, "v6 res_mkquery 2nd");
+
+ fd1 = android_res_nsend(handle, buf1, len1, 0);
+ EXPECT_GE(env, fd1, 0, "v6 res_nsend 1st");
+ fd2 = android_res_nsend(handle, buf2, len2, 0);
+ EXPECT_GE(env, fd2, 0, "v6 res_nsend 2nd");
+
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd2, AF_INET6, ns_r_noerror),
+ "v6 res_nsend 2nd check answers");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd1, AF_INET6, ns_r_noerror),
+ "v6 res_nsend 1st check answers");
+}
+
+extern "C"
+JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNnxDomainCheck(
+ JNIEnv* env, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+
+ // res_nquery V4 NXDOMAIN
+ int fd = android_res_nquery(handle, kNxDomainName, ns_c_in, ns_t_a, 0);
+ EXPECT_GE(env, fd, 0, "v4 res_nquery NXDOMAIN");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_nxdomain),
+ "v4 res_nquery NXDOMAIN check answers");
+
+ // res_nquery V6 NXDOMAIN
+ fd = android_res_nquery(handle, kNxDomainName, ns_c_in, ns_t_aaaa, 0);
+ EXPECT_GE(env, fd, 0, "v6 res_nquery NXDOMAIN");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET6, ns_r_nxdomain),
+ "v6 res_nquery NXDOMAIN check answers");
+
+ uint8_t buf[MAXPACKET] = {};
+ // res_nsend V4 NXDOMAIN
+ int len = makeQuery(kNxDomainName, ns_t_a, buf, sizeof(buf));
+ EXPECT_GT(env, len, 0, "v4 res_mkquery NXDOMAIN");
+ fd = android_res_nsend(handle, buf, len, 0);
+ EXPECT_GE(env, fd, 0, "v4 res_nsend NXDOMAIN");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_nxdomain),
+ "v4 res_nsend NXDOMAIN check answers");
+
+ // res_nsend V6 NXDOMAIN
+ memset(buf, 0, sizeof(buf));
+ len = makeQuery(kNxDomainName, ns_t_aaaa, buf, sizeof(buf));
+ EXPECT_GT(env, len, 0, "v6 res_mkquery NXDOMAIN");
+ fd = android_res_nsend(handle, buf, len, 0);
+ EXPECT_GE(env, fd, 0, "v6 res_nsend NXDOMAIN");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET6, ns_r_nxdomain),
+ "v6 res_nsend NXDOMAIN check answers");
+}
+
+
+extern "C"
+JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNcancelCheck(
+ JNIEnv* env, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+
+ int fd = android_res_nquery(handle, kGoogleName, ns_c_in, ns_t_a, 0);
+ errno = 0;
+ android_res_cancel(fd);
+ int err = errno;
+ EXPECT_EQ(env, 0, err, "res_cancel");
+ // DO NOT call cancel or result with the same fd more than once,
+ // otherwise it will hit fdsan double-close fd.
+}
+
+extern "C"
+JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNapiMalformedCheck(
+ JNIEnv* env, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+
+ // It is the equivalent of "dig . a", Query with an empty name.
+ int fd = android_res_nquery(handle, "", ns_c_in, ns_t_a, 0);
+ EXPECT_GE(env, fd, 0, "res_nquery root");
+ EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_noerror),
+ "res_nquery root check answers");
+
+ // Label limit 63
+ std::string exceedingLabelQuery = "www." + std::string(70, 'g') + ".com";
+ // Name limit 255
+ std::string exceedingDomainQuery = "www." + std::string(255, 'g') + ".com";
+
+ fd = android_res_nquery(handle, exceedingLabelQuery.c_str(), ns_c_in, ns_t_a, 0);
+ EXPECT_EQ(env, -EMSGSIZE, fd, "res_nquery exceedingLabelQuery");
+ fd = android_res_nquery(handle, exceedingDomainQuery.c_str(), ns_c_in, ns_t_aaaa, 0);
+ EXPECT_EQ(env, -EMSGSIZE, fd, "res_nquery exceedingDomainQuery");
+
+ uint8_t buf[10] = {};
+ // empty BLOB
+ fd = android_res_nsend(handle, buf, 10, 0);
+ EXPECT_GE(env, fd, 0, "res_nsend empty BLOB");
+ EXPECT_EQ(env, 0, expectAnswersNotValid(env, fd, -EINVAL),
+ "res_nsend empty BLOB check answers");
+
+ uint8_t largeBuf[2 * MAXPACKET] = {};
+ // A buffer larger than 8KB
+ fd = android_res_nsend(handle, largeBuf, sizeof(largeBuf), 0);
+ EXPECT_EQ(env, -EMSGSIZE, fd, "res_nsend buffer larger than 8KB");
+
+ // 5000 bytes filled with 0. This returns EMSGSIZE because FrameworkListener limits the size of
+ // commands to 4096 bytes.
+ fd = android_res_nsend(handle, largeBuf, 5000, 0);
+ EXPECT_EQ(env, -EMSGSIZE, fd, "res_nsend 5000 bytes filled with 0");
+
+ // 500 bytes filled with 0
+ fd = android_res_nsend(handle, largeBuf, 500, 0);
+ EXPECT_GE(env, fd, 0, "res_nsend 500 bytes filled with 0");
+ EXPECT_EQ(env, 0, expectAnswersNotValid(env, fd, -EINVAL),
+ "res_nsend 500 bytes filled with 0 check answers");
+
+ // 5000 bytes filled with 0xFF
+ uint8_t ffBuf[5001] = {};
+ memset(ffBuf, 0xFF, sizeof(ffBuf));
+ ffBuf[5000] = '\0';
+ fd = android_res_nsend(handle, ffBuf, sizeof(ffBuf), 0);
+ EXPECT_EQ(env, -EMSGSIZE, fd, "res_nsend 5000 bytes filled with 0xFF");
+
+ // 500 bytes filled with 0xFF
+ ffBuf[500] = '\0';
+ fd = android_res_nsend(handle, ffBuf, 501, 0);
+ EXPECT_GE(env, fd, 0, "res_nsend 500 bytes filled with 0xFF");
+ EXPECT_EQ(env, 0, expectAnswersNotValid(env, fd, -EINVAL),
+ "res_nsend 500 bytes filled with 0xFF check answers");
+}
+
+extern "C"
+JNIEXPORT jint Java_android_net_cts_MultinetworkApiTest_runGetaddrinfoCheck(
+ JNIEnv*, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+ struct addrinfo *res = NULL;
+
+ errno = 0;
+ int rval = android_getaddrinfofornetwork(handle, kHostname, NULL, NULL, &res);
+ const int saved_errno = errno;
+ freeaddrinfo(res);
+
+ LOGD("android_getaddrinfofornetwork(%" PRIu64 ", %s) returned rval=%d errno=%d",
+ handle, kHostname, rval, saved_errno);
+ return rval == 0 ? 0 : -saved_errno;
+}
+
+extern "C"
+JNIEXPORT jint Java_android_net_cts_MultinetworkApiTest_runSetprocnetwork(
+ JNIEnv*, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+
+ errno = 0;
+ int rval = android_setprocnetwork(handle);
+ const int saved_errno = errno;
+ LOGD("android_setprocnetwork(%" PRIu64 ") returned rval=%d errno=%d",
+ handle, rval, saved_errno);
+ return rval == 0 ? 0 : -saved_errno;
+}
+
+extern "C"
+JNIEXPORT jint Java_android_net_cts_MultinetworkApiTest_runSetsocknetwork(
+ JNIEnv*, jclass, jlong nethandle) {
+ net_handle_t handle = (net_handle_t) nethandle;
+
+ errno = 0;
+ int fd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
+ if (fd < 0) {
+ LOGD("socket() failed, errno=%d", errno);
+ return -errno;
+ }
+
+ errno = 0;
+ int rval = android_setsocknetwork(handle, fd);
+ const int saved_errno = errno;
+ LOGD("android_setprocnetwork(%" PRIu64 ", %d) returned rval=%d errno=%d",
+ handle, fd, rval, saved_errno);
+ close(fd);
+ return rval == 0 ? 0 : -saved_errno;
+}
+
+// Use sizeof("x") - 1 because we need a compile-time constant, and strlen("x")
+// isn't guaranteed to fold to a constant.
+static const int kSockaddrStrLen = INET6_ADDRSTRLEN + sizeof("[]:65535") - 1;
+
+void sockaddr_ntop(const struct sockaddr *sa, socklen_t salen, char *dst, const size_t size) {
+ char addrstr[INET6_ADDRSTRLEN];
+ char portstr[sizeof("65535")];
+ char buf[kSockaddrStrLen+1];
+
+ int ret = getnameinfo(sa, salen,
+ addrstr, sizeof(addrstr),
+ portstr, sizeof(portstr),
+ NI_NUMERICHOST | NI_NUMERICSERV);
+ if (ret == 0) {
+ snprintf(buf, sizeof(buf),
+ (sa->sa_family == AF_INET6) ? "[%s]:%s" : "%s:%s",
+ addrstr, portstr);
+ } else {
+ sprintf(buf, "???");
+ }
+
+ strlcpy(dst, buf, size);
+}
+
+extern "C"
+JNIEXPORT jint Java_android_net_cts_MultinetworkApiTest_runDatagramCheck(
+ JNIEnv*, jclass, jlong nethandle) {
+ const struct addrinfo kHints = {
+ .ai_flags = AI_ADDRCONFIG,
+ .ai_family = AF_UNSPEC,
+ .ai_socktype = SOCK_DGRAM,
+ .ai_protocol = IPPROTO_UDP,
+ };
+ struct addrinfo *res = NULL;
+ net_handle_t handle = (net_handle_t) nethandle;
+
+ static const char kPort[] = "443";
+ int rval = android_getaddrinfofornetwork(handle, kHostname, kPort, &kHints, &res);
+ if (rval != 0) {
+ LOGD("android_getaddrinfofornetwork(%llu, %s) returned rval=%d errno=%d",
+ handle, kHostname, rval, errno);
+ freeaddrinfo(res);
+ return -errno;
+ }
+
+ // Rely upon getaddrinfo sorting the best destination to the front.
+ int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+ if (fd < 0) {
+ LOGD("socket(%d, %d, %d) failed, errno=%d",
+ res->ai_family, res->ai_socktype, res->ai_protocol, errno);
+ freeaddrinfo(res);
+ return -errno;
+ }
+
+ rval = android_setsocknetwork(handle, fd);
+ LOGD("android_setprocnetwork(%llu, %d) returned rval=%d errno=%d",
+ handle, fd, rval, errno);
+ if (rval != 0) {
+ close(fd);
+ freeaddrinfo(res);
+ return -errno;
+ }
+
+ char addrstr[kSockaddrStrLen+1];
+ sockaddr_ntop(res->ai_addr, res->ai_addrlen, addrstr, sizeof(addrstr));
+ LOGD("Attempting connect() to %s ...", addrstr);
+
+ rval = connect(fd, res->ai_addr, res->ai_addrlen);
+ if (rval != 0) {
+ close(fd);
+ freeaddrinfo(res);
+ return -errno;
+ }
+ freeaddrinfo(res);
+
+ struct sockaddr_storage src_addr;
+ socklen_t src_addrlen = sizeof(src_addr);
+ if (getsockname(fd, (struct sockaddr *)&src_addr, &src_addrlen) != 0) {
+ close(fd);
+ return -errno;
+ }
+ sockaddr_ntop((const struct sockaddr *)&src_addr, sizeof(src_addr), addrstr, sizeof(addrstr));
+ LOGD("... from %s", addrstr);
+
+ // Don't let reads or writes block indefinitely.
+ const struct timeval timeo = { 2, 0 }; // 2 seconds
+ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeo, sizeof(timeo));
+ setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeo, sizeof(timeo));
+
+ // For reference see:
+ // https://datatracker.ietf.org/doc/html/draft-ietf-quic-invariants
+ uint8_t quic_packet[1200] = {
+ 0xc0, // long header
+ 0xaa, 0xda, 0xca, 0xca, // reserved-space version number
+ 0x08, // destination connection ID length
+ 0, 0, 0, 0, 0, 0, 0, 0, // 64bit connection ID
+ 0x00, // source connection ID length
+ };
+
+ arc4random_buf(quic_packet + 6, 8); // random connection ID
+
+ uint8_t response[1500];
+ ssize_t sent, rcvd;
+ static const int MAX_RETRIES = 5;
+ int i, errnum = 0;
+
+ for (i = 0; i < MAX_RETRIES; i++) {
+ sent = send(fd, quic_packet, sizeof(quic_packet), 0);
+ if (sent < (ssize_t)sizeof(quic_packet)) {
+ errnum = errno;
+ LOGD("send(QUIC packet) returned sent=%zd, errno=%d", sent, errnum);
+ close(fd);
+ return -errnum;
+ }
+
+ rcvd = recv(fd, response, sizeof(response), 0);
+ if (rcvd > 0) {
+ break;
+ } else {
+ errnum = errno;
+ LOGD("[%d/%d] recv(QUIC response) returned rcvd=%zd, errno=%d",
+ i + 1, MAX_RETRIES, rcvd, errnum);
+ }
+ }
+ if (rcvd < 15) {
+ LOGD("QUIC UDP %s: sent=%zd but rcvd=%zd, errno=%d", kPort, sent, rcvd, errnum);
+ if (rcvd <= 0) {
+ LOGD("Does this network block UDP port %s?", kPort);
+ }
+ close(fd);
+ return -EPROTO;
+ }
+
+ int conn_id_cmp = memcmp(quic_packet + 6, response + 7, 8);
+ if (conn_id_cmp != 0) {
+ LOGD("sent and received connection IDs do not match");
+ close(fd);
+ return -EPROTO;
+ }
+
+ // TODO: Replace this quick 'n' dirty test with proper QUIC-capable code.
+
+ close(fd);
+ return 0;
+}
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
new file mode 100644
index 0000000..6defd35
--- /dev/null
+++ b/tests/cts/net/native/dns/Android.bp
@@ -0,0 +1,41 @@
+cc_defaults {
+ name: "dns_async_defaults",
+
+ cflags: [
+ "-fstack-protector-all",
+ "-g",
+ "-Wall",
+ "-Wextra",
+ "-Werror",
+ "-Wnullable-to-nonnull-conversion",
+ "-Wsign-compare",
+ "-Wthread-safety",
+ "-Wunused-parameter",
+ ],
+ srcs: [
+ "NativeDnsAsyncTest.cpp",
+ ],
+ shared_libs: [
+ "libandroid",
+ "liblog",
+ "libutils",
+ ],
+}
+
+cc_test {
+ name: "CtsNativeNetDnsTestCases",
+ defaults: ["dns_async_defaults"],
+ multilib: {
+ lib32: {
+ suffix: "32",
+ },
+ lib64: {
+ suffix: "64",
+ },
+ },
+ test_suites: [
+ "cts",
+ "general-tests",
+ "mts",
+ ],
+}
diff --git a/tests/cts/net/native/dns/AndroidTest.xml b/tests/cts/net/native/dns/AndroidTest.xml
new file mode 100644
index 0000000..6d03c23
--- /dev/null
+++ b/tests/cts/net/native/dns/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+<configuration description="Config for CTS Native Network dns test cases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="networking" />
+ <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+ <option name="cleanup" value="true" />
+ <option name="push" value="CtsNativeNetDnsTestCases->/data/local/tmp/CtsNativeNetDnsTestCases" />
+ <option name="append-bitness" value="true" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.GTest" >
+ <option name="native-test-device-path" value="/data/local/tmp" />
+ <option name="module-name" value="CtsNativeNetDnsTestCases" />
+ <option name="runtime-hint" value="1m" />
+ </test>
+</configuration>
diff --git a/tests/cts/net/native/dns/NativeDnsAsyncTest.cpp b/tests/cts/net/native/dns/NativeDnsAsyncTest.cpp
new file mode 100644
index 0000000..e501475
--- /dev/null
+++ b/tests/cts/net/native/dns/NativeDnsAsyncTest.cpp
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#include <arpa/inet.h>
+#include <arpa/nameser.h>
+#include <error.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <netinet/in.h>
+#include <poll.h> /* poll */
+#include <resolv.h>
+#include <string.h>
+#include <sys/socket.h>
+
+#include <android/multinetwork.h>
+#include <gtest/gtest.h>
+
+namespace {
+constexpr int MAXPACKET = 8 * 1024;
+constexpr int PTON_MAX = 16;
+constexpr int TIMEOUT_MS = 10000;
+
+int getAsyncResponse(int fd, int timeoutMs, int* rcode, uint8_t* buf, size_t bufLen) {
+ struct pollfd wait_fd[1];
+ wait_fd[0].fd = fd;
+ wait_fd[0].events = POLLIN;
+ short revents;
+ int ret;
+ ret = poll(wait_fd, 1, timeoutMs);
+ revents = wait_fd[0].revents;
+ if (revents & POLLIN) {
+ int n = android_res_nresult(fd, rcode, buf, bufLen);
+ // Verify that android_res_nresult() closed the fd
+ char dummy;
+ EXPECT_EQ(-1, read(fd, &dummy, sizeof dummy));
+ EXPECT_EQ(EBADF, errno);
+ return n;
+ }
+
+ return -1;
+}
+
+std::vector<std::string> extractIpAddressAnswers(uint8_t* buf, size_t bufLen, int ipType) {
+ ns_msg handle;
+ if (ns_initparse((const uint8_t*) buf, bufLen, &handle) < 0) {
+ return {};
+ }
+ const int ancount = ns_msg_count(handle, ns_s_an);
+ ns_rr rr;
+ std::vector<std::string> answers;
+ for (int i = 0; i < ancount; i++) {
+ if (ns_parserr(&handle, ns_s_an, i, &rr) < 0) {
+ continue;
+ }
+ const uint8_t* rdata = ns_rr_rdata(rr);
+ char buffer[INET6_ADDRSTRLEN];
+ if (inet_ntop(ipType, (const char*) rdata, buffer, sizeof(buffer))) {
+ answers.push_back(buffer);
+ }
+ }
+ return answers;
+}
+
+void expectAnswersValid(int fd, int ipType, int expectedRcode) {
+ int rcode = -1;
+ uint8_t buf[MAXPACKET] = {};
+ int res = getAsyncResponse(fd, TIMEOUT_MS, &rcode, buf, MAXPACKET);
+ EXPECT_GE(res, 0);
+ EXPECT_EQ(rcode, expectedRcode);
+
+ if (expectedRcode == ns_r_noerror) {
+ auto answers = extractIpAddressAnswers(buf, res, ipType);
+ EXPECT_GE(answers.size(), 0U);
+ for (auto &answer : answers) {
+ char pton[PTON_MAX];
+ EXPECT_EQ(1, inet_pton(ipType, answer.c_str(), pton));
+ }
+ }
+}
+
+void expectAnswersNotValid(int fd, int expectedErrno) {
+ int rcode = -1;
+ uint8_t buf[MAXPACKET] = {};
+ int res = getAsyncResponse(fd, TIMEOUT_MS, &rcode, buf, MAXPACKET);
+ EXPECT_EQ(expectedErrno, res);
+}
+
+} // namespace
+
+TEST (NativeDnsAsyncTest, Async_Query) {
+ // V4
+ int fd1 = android_res_nquery(
+ NETWORK_UNSPECIFIED, "www.google.com", ns_c_in, ns_t_a, 0);
+ EXPECT_GE(fd1, 0);
+ int fd2 = android_res_nquery(
+ NETWORK_UNSPECIFIED, "www.youtube.com", ns_c_in, ns_t_a, 0);
+ EXPECT_GE(fd2, 0);
+ expectAnswersValid(fd2, AF_INET, ns_r_noerror);
+ expectAnswersValid(fd1, AF_INET, ns_r_noerror);
+
+ // V6
+ fd1 = android_res_nquery(
+ NETWORK_UNSPECIFIED, "www.google.com", ns_c_in, ns_t_aaaa, 0);
+ EXPECT_GE(fd1, 0);
+ fd2 = android_res_nquery(
+ NETWORK_UNSPECIFIED, "www.youtube.com", ns_c_in, ns_t_aaaa, 0);
+ EXPECT_GE(fd2, 0);
+ expectAnswersValid(fd2, AF_INET6, ns_r_noerror);
+ expectAnswersValid(fd1, AF_INET6, ns_r_noerror);
+}
+
+TEST (NativeDnsAsyncTest, Async_Send) {
+ // V4
+ uint8_t buf1[MAXPACKET] = {};
+ int len1 = res_mkquery(ns_o_query, "www.googleapis.com",
+ ns_c_in, ns_t_a, nullptr, 0, nullptr, buf1, sizeof(buf1));
+ EXPECT_GT(len1, 0);
+
+ uint8_t buf2[MAXPACKET] = {};
+ int len2 = res_mkquery(ns_o_query, "play.googleapis.com",
+ ns_c_in, ns_t_a, nullptr, 0, nullptr, buf2, sizeof(buf2));
+ EXPECT_GT(len2, 0);
+
+ int fd1 = android_res_nsend(NETWORK_UNSPECIFIED, buf1, len1, 0);
+ EXPECT_GE(fd1, 0);
+ int fd2 = android_res_nsend(NETWORK_UNSPECIFIED, buf2, len2, 0);
+ EXPECT_GE(fd2, 0);
+
+ expectAnswersValid(fd2, AF_INET, ns_r_noerror);
+ expectAnswersValid(fd1, AF_INET, ns_r_noerror);
+
+ // V6
+ memset(buf1, 0, sizeof(buf1));
+ memset(buf2, 0, sizeof(buf2));
+ len1 = res_mkquery(ns_o_query, "www.googleapis.com",
+ ns_c_in, ns_t_aaaa, nullptr, 0, nullptr, buf1, sizeof(buf1));
+ EXPECT_GT(len1, 0);
+ len2 = res_mkquery(ns_o_query, "play.googleapis.com",
+ ns_c_in, ns_t_aaaa, nullptr, 0, nullptr, buf2, sizeof(buf2));
+ EXPECT_GT(len2, 0);
+
+ fd1 = android_res_nsend(NETWORK_UNSPECIFIED, buf1, len1, 0);
+ EXPECT_GE(fd1, 0);
+ fd2 = android_res_nsend(NETWORK_UNSPECIFIED, buf2, len2, 0);
+ EXPECT_GE(fd2, 0);
+
+ expectAnswersValid(fd2, AF_INET6, ns_r_noerror);
+ expectAnswersValid(fd1, AF_INET6, ns_r_noerror);
+}
+
+TEST (NativeDnsAsyncTest, Async_NXDOMAIN) {
+ uint8_t buf[MAXPACKET] = {};
+ int len = res_mkquery(ns_o_query, "test1-nx.metric.gstatic.com",
+ ns_c_in, ns_t_a, nullptr, 0, nullptr, buf, sizeof(buf));
+ EXPECT_GT(len, 0);
+ int fd1 = android_res_nsend(NETWORK_UNSPECIFIED, buf, len, ANDROID_RESOLV_NO_CACHE_LOOKUP);
+ EXPECT_GE(fd1, 0);
+
+ len = res_mkquery(ns_o_query, "test2-nx.metric.gstatic.com",
+ ns_c_in, ns_t_a, nullptr, 0, nullptr, buf, sizeof(buf));
+ EXPECT_GT(len, 0);
+ int fd2 = android_res_nsend(NETWORK_UNSPECIFIED, buf, len, ANDROID_RESOLV_NO_CACHE_LOOKUP);
+ EXPECT_GE(fd2, 0);
+
+ expectAnswersValid(fd2, AF_INET, ns_r_nxdomain);
+ expectAnswersValid(fd1, AF_INET, ns_r_nxdomain);
+
+ fd1 = android_res_nquery(
+ NETWORK_UNSPECIFIED, "test3-nx.metric.gstatic.com",
+ ns_c_in, ns_t_aaaa, ANDROID_RESOLV_NO_CACHE_LOOKUP);
+ EXPECT_GE(fd1, 0);
+ fd2 = android_res_nquery(
+ NETWORK_UNSPECIFIED, "test4-nx.metric.gstatic.com",
+ ns_c_in, ns_t_aaaa, ANDROID_RESOLV_NO_CACHE_LOOKUP);
+ EXPECT_GE(fd2, 0);
+ expectAnswersValid(fd2, AF_INET6, ns_r_nxdomain);
+ expectAnswersValid(fd1, AF_INET6, ns_r_nxdomain);
+}
+
+TEST (NativeDnsAsyncTest, Async_Cancel) {
+ int fd = android_res_nquery(
+ NETWORK_UNSPECIFIED, "www.google.com", ns_c_in, ns_t_a, 0);
+ errno = 0;
+ android_res_cancel(fd);
+ int err = errno;
+ EXPECT_EQ(err, 0);
+ // DO NOT call cancel or result with the same fd more than once,
+ // otherwise it will hit fdsan double-close fd.
+}
+
+TEST (NativeDnsAsyncTest, Async_Query_MALFORMED) {
+ // Empty string to create BLOB and query, we will get empty result and rcode = 0
+ // on DNSTLS.
+ int fd = android_res_nquery(
+ NETWORK_UNSPECIFIED, "", ns_c_in, ns_t_a, 0);
+ EXPECT_GE(fd, 0);
+ expectAnswersValid(fd, AF_INET, ns_r_noerror);
+
+ std::string exceedingLabelQuery = "www." + std::string(70, 'g') + ".com";
+ std::string exceedingDomainQuery = "www." + std::string(255, 'g') + ".com";
+
+ fd = android_res_nquery(NETWORK_UNSPECIFIED,
+ exceedingLabelQuery.c_str(), ns_c_in, ns_t_a, 0);
+ EXPECT_EQ(-EMSGSIZE, fd);
+ fd = android_res_nquery(NETWORK_UNSPECIFIED,
+ exceedingDomainQuery.c_str(), ns_c_in, ns_t_a, 0);
+ EXPECT_EQ(-EMSGSIZE, fd);
+}
+
+TEST (NativeDnsAsyncTest, Async_Send_MALFORMED) {
+ uint8_t buf[10] = {};
+ // empty BLOB
+ int fd = android_res_nsend(NETWORK_UNSPECIFIED, buf, 10, 0);
+ EXPECT_GE(fd, 0);
+ expectAnswersNotValid(fd, -EINVAL);
+
+ std::vector<uint8_t> largeBuf(2 * MAXPACKET, 0);
+ // A buffer larger than 8KB
+ fd = android_res_nsend(
+ NETWORK_UNSPECIFIED, largeBuf.data(), largeBuf.size(), 0);
+ EXPECT_EQ(-EMSGSIZE, fd);
+
+ // 5000 bytes filled with 0. This returns EMSGSIZE because FrameworkListener limits the size of
+ // commands to 4096 bytes.
+ fd = android_res_nsend(NETWORK_UNSPECIFIED, largeBuf.data(), 5000, 0);
+ EXPECT_EQ(-EMSGSIZE, fd);
+
+ // 500 bytes filled with 0
+ fd = android_res_nsend(NETWORK_UNSPECIFIED, largeBuf.data(), 500, 0);
+ EXPECT_GE(fd, 0);
+ expectAnswersNotValid(fd, -EINVAL);
+
+ // 5000 bytes filled with 0xFF
+ std::vector<uint8_t> ffBuf(5000, 0xFF);
+ fd = android_res_nsend(
+ NETWORK_UNSPECIFIED, ffBuf.data(), ffBuf.size(), 0);
+ EXPECT_EQ(-EMSGSIZE, fd);
+
+ // 500 bytes filled with 0xFF
+ fd = android_res_nsend(NETWORK_UNSPECIFIED, ffBuf.data(), 500, 0);
+ EXPECT_GE(fd, 0);
+ expectAnswersNotValid(fd, -EINVAL);
+}
diff --git a/tests/cts/net/native/qtaguid/Android.bp b/tests/cts/net/native/qtaguid/Android.bp
new file mode 100644
index 0000000..4861651
--- /dev/null
+++ b/tests/cts/net/native/qtaguid/Android.bp
@@ -0,0 +1,53 @@
+// Copyright (C) 2017 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.
+
+// Build the unit tests.
+
+cc_test {
+ name: "CtsNativeNetTestCases",
+
+ compile_multilib: "both",
+ multilib: {
+ lib32: {
+ suffix: "32",
+ },
+ lib64: {
+ suffix: "64",
+ },
+ },
+
+ srcs: ["src/NativeQtaguidTest.cpp"],
+
+ shared_libs: [
+ "libutils",
+ "liblog",
+ ],
+
+ static_libs: [
+ "libgtest",
+ "libqtaguid",
+ ],
+
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+
+ cflags: [
+ "-Werror",
+ "-Wall",
+ ],
+
+}
diff --git a/tests/cts/net/native/qtaguid/AndroidTest.xml b/tests/cts/net/native/qtaguid/AndroidTest.xml
new file mode 100644
index 0000000..fa4b2cf
--- /dev/null
+++ b/tests/cts/net/native/qtaguid/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2017 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.
+-->
+<configuration description="Config for CTS Native Network xt_qtaguid test cases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="networking" />
+ <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+ <option name="cleanup" value="true" />
+ <option name="push" value="CtsNativeNetTestCases->/data/local/tmp/CtsNativeNetTestCases" />
+ <option name="append-bitness" value="true" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.GTest" >
+ <option name="native-test-device-path" value="/data/local/tmp" />
+ <option name="module-name" value="CtsNativeNetTestCases" />
+ <option name="runtime-hint" value="1m" />
+ </test>
+</configuration>
diff --git a/tests/cts/net/native/qtaguid/src/NativeQtaguidTest.cpp b/tests/cts/net/native/qtaguid/src/NativeQtaguidTest.cpp
new file mode 100644
index 0000000..7dc6240
--- /dev/null
+++ b/tests/cts/net/native/qtaguid/src/NativeQtaguidTest.cpp
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#include <arpa/inet.h>
+#include <error.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <fcntl.h>
+#include <string.h>
+#include <sys/socket.h>
+
+#include <gtest/gtest.h>
+#include <qtaguid/qtaguid.h>
+
+int canAccessQtaguidFile() {
+ int fd = open("/proc/net/xt_qtaguid/ctrl", O_RDONLY | O_CLOEXEC);
+ close(fd);
+ return fd != -1;
+}
+
+#define SKIP_IF_QTAGUID_NOT_SUPPORTED() \
+ do { \
+ int res = canAccessQtaguidFile(); \
+ ASSERT_LE(0, res); \
+ if (!res) { \
+ GTEST_LOG_(INFO) << "This test is skipped since kernel may not have the module\n"; \
+ return; \
+ } \
+ } while (0)
+
+int getCtrlSkInfo(int tag, uid_t uid, uint64_t* sk_addr, int* ref_cnt) {
+ FILE *fp;
+ fp = fopen("/proc/net/xt_qtaguid/ctrl", "r");
+ if (!fp)
+ return -ENOENT;
+ uint64_t full_tag = (uint64_t)tag << 32 | uid;
+ char pattern[40];
+ snprintf(pattern, sizeof(pattern), " tag=0x%" PRIx64 " (uid=%" PRIu32 ")", full_tag, uid);
+
+ size_t len;
+ char *line_buffer = NULL;
+ while(getline(&line_buffer, &len, fp) != -1) {
+ if (strstr(line_buffer, pattern) == NULL)
+ continue;
+ int res;
+ pid_t dummy_pid;
+ uint64_t k_tag;
+ uint32_t k_uid;
+ const int TOTAL_PARAM = 5;
+ res = sscanf(line_buffer, "sock=%" PRIx64 " tag=0x%" PRIx64 " (uid=%" PRIu32 ") "
+ "pid=%u f_count=%u", sk_addr, &k_tag, &k_uid,
+ &dummy_pid, ref_cnt);
+ if (!(res == TOTAL_PARAM && k_tag == full_tag && k_uid == uid))
+ return -EINVAL;
+ free(line_buffer);
+ return 0;
+ }
+ free(line_buffer);
+ return -ENOENT;
+}
+
+void checkNoSocketPointerLeaks(int family) {
+ int sockfd = socket(family, SOCK_STREAM, 0);
+ uid_t uid = getuid();
+ int tag = arc4random();
+ int ref_cnt;
+ uint64_t sk_addr;
+ uint64_t expect_addr = 0;
+
+ EXPECT_EQ(0, legacy_tagSocket(sockfd, tag, uid));
+ EXPECT_EQ(0, getCtrlSkInfo(tag, uid, &sk_addr, &ref_cnt));
+ EXPECT_EQ(expect_addr, sk_addr);
+ close(sockfd);
+ EXPECT_EQ(-ENOENT, getCtrlSkInfo(tag, uid, &sk_addr, &ref_cnt));
+}
+
+TEST (NativeQtaguidTest, close_socket_without_untag) {
+ SKIP_IF_QTAGUID_NOT_SUPPORTED();
+
+ int sockfd = socket(AF_INET, SOCK_STREAM, 0);
+ uid_t uid = getuid();
+ int tag = arc4random();
+ int ref_cnt;
+ uint64_t dummy_sk;
+ EXPECT_EQ(0, legacy_tagSocket(sockfd, tag, uid));
+ EXPECT_EQ(0, getCtrlSkInfo(tag, uid, &dummy_sk, &ref_cnt));
+ EXPECT_EQ(2, ref_cnt);
+ close(sockfd);
+ EXPECT_EQ(-ENOENT, getCtrlSkInfo(tag, uid, &dummy_sk, &ref_cnt));
+}
+
+TEST (NativeQtaguidTest, close_socket_without_untag_ipv6) {
+ SKIP_IF_QTAGUID_NOT_SUPPORTED();
+
+ int sockfd = socket(AF_INET6, SOCK_STREAM, 0);
+ uid_t uid = getuid();
+ int tag = arc4random();
+ int ref_cnt;
+ uint64_t dummy_sk;
+ EXPECT_EQ(0, legacy_tagSocket(sockfd, tag, uid));
+ EXPECT_EQ(0, getCtrlSkInfo(tag, uid, &dummy_sk, &ref_cnt));
+ EXPECT_EQ(2, ref_cnt);
+ close(sockfd);
+ EXPECT_EQ(-ENOENT, getCtrlSkInfo(tag, uid, &dummy_sk, &ref_cnt));
+}
+
+TEST (NativeQtaguidTest, no_socket_addr_leak) {
+ SKIP_IF_QTAGUID_NOT_SUPPORTED();
+
+ checkNoSocketPointerLeaks(AF_INET);
+ checkNoSocketPointerLeaks(AF_INET6);
+}
+
+int main(int argc, char **argv) {
+ testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
diff --git a/tests/cts/net/src/android/net/cts/AirplaneModeTest.java b/tests/cts/net/src/android/net/cts/AirplaneModeTest.java
new file mode 100644
index 0000000..524e549
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/AirplaneModeTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2016 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.ContentResolver;
+import android.content.Context;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import java.lang.Thread;
+
+@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
+public class AirplaneModeTest extends AndroidTestCase {
+ private static final String TAG = "AirplaneModeTest";
+ private static final String FEATURE_BLUETOOTH = "android.hardware.bluetooth";
+ private static final String FEATURE_WIFI = "android.hardware.wifi";
+ private static final int TIMEOUT_MS = 10 * 1000;
+ private boolean mHasFeature;
+ private Context mContext;
+ private ContentResolver resolver;
+
+ public void setup() {
+ mContext= getContext();
+ resolver = mContext.getContentResolver();
+ mHasFeature = (mContext.getPackageManager().hasSystemFeature(FEATURE_BLUETOOTH)
+ || mContext.getPackageManager().hasSystemFeature(FEATURE_WIFI));
+ }
+
+ public void testAirplaneMode() {
+ setup();
+ if (!mHasFeature) {
+ Log.i(TAG, "The device doesn't support network bluetooth or wifi feature");
+ return;
+ }
+
+ for (int testCount = 0; testCount < 2; testCount++) {
+ if (!doOneTest()) {
+ fail("Airplane mode failed to change in " + TIMEOUT_MS + "msec");
+ return;
+ }
+ }
+ }
+
+ private boolean doOneTest() {
+ boolean airplaneModeOn = isAirplaneModeOn();
+ setAirplaneModeOn(!airplaneModeOn);
+
+ try {
+ Thread.sleep(TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Sleep time interrupted.", e);
+ }
+
+ if (airplaneModeOn == isAirplaneModeOn()) {
+ return false;
+ }
+ return true;
+ }
+
+ private void setAirplaneModeOn(boolean enabling) {
+ // Change the system setting for airplane mode
+ Settings.Global.putInt(resolver, Settings.Global.AIRPLANE_MODE_ON, enabling ? 1 : 0);
+ }
+
+ private boolean isAirplaneModeOn() {
+ // Read the system setting for airplane mode
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
new file mode 100644
index 0000000..eb5048f
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2020 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.CONNECTIVITY_INTERNAL
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.Uri
+import android.net.cts.NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig
+import android.net.cts.NetworkValidationTestUtil.setHttpUrlDeviceConfig
+import android.net.cts.NetworkValidationTestUtil.setHttpsUrlDeviceConfig
+import android.net.cts.NetworkValidationTestUtil.setUrlExpirationDeviceConfig
+import android.net.cts.util.CtsNetUtils
+import android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL
+import android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL
+import android.net.wifi.WifiManager
+import android.os.Build
+import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig
+import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
+import android.text.TextUtils
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.TestHttpServer
+import com.android.testutils.TestHttpServer.Request
+import com.android.testutils.isDevSdkInRange
+import com.android.testutils.runAsShell
+import fi.iki.elonen.NanoHTTPD.Response.Status
+import junit.framework.AssertionFailedError
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.runner.RunWith
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+import kotlin.test.Test
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+private const val TEST_HTTPS_URL_PATH = "/https_path"
+private const val TEST_HTTP_URL_PATH = "/http_path"
+private const val TEST_PORTAL_URL_PATH = "/portal_path"
+
+private const val LOCALHOST_HOSTNAME = "localhost"
+
+// Re-connecting to the AP, obtaining an IP address, revalidating can take a long time
+private const val WIFI_CONNECT_TIMEOUT_MS = 120_000L
+private const val TEST_TIMEOUT_MS = 10_000L
+
+private fun <T> CompletableFuture<T>.assertGet(timeoutMs: Long, message: String): T {
+ try {
+ return get(timeoutMs, TimeUnit.MILLISECONDS)
+ } catch (e: TimeoutException) {
+ throw AssertionFailedError(message)
+ }
+}
+
+@AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
+@RunWith(AndroidJUnit4::class)
+class CaptivePortalTest {
+ private val context: android.content.Context by lazy { getInstrumentation().context }
+ private val wm by lazy { context.getSystemService(WifiManager::class.java) }
+ private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
+ private val pm by lazy { context.packageManager }
+ private val utils by lazy { CtsNetUtils(context) }
+
+ private val server = TestHttpServer("localhost")
+
+ @Before
+ fun setUp() {
+ runAsShell(READ_DEVICE_CONFIG) {
+ // Verify that the test URLs are not normally set on the device, but do not fail if the
+ // test URLs are set to what this test uses (URLs on localhost), in case the test was
+ // interrupted manually and rerun.
+ assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
+ assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
+ }
+ clearValidationTestUrlsDeviceConfig()
+ server.start()
+ }
+
+ @After
+ fun tearDown() {
+ clearValidationTestUrlsDeviceConfig()
+ if (pm.hasSystemFeature(FEATURE_WIFI)) {
+ reconnectWifi()
+ }
+ server.stop()
+ }
+
+ private fun assertEmptyOrLocalhostUrl(urlKey: String) {
+ val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
+ assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
+ "$urlKey must not be set in production scenarios (current value: $url)")
+ }
+
+ @Test
+ fun testCaptivePortalIsNotDefaultNetwork() {
+ assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
+ assumeTrue(pm.hasSystemFeature(FEATURE_WIFI))
+ utils.ensureWifiConnected()
+ utils.connectToCell()
+
+ // Have network validation use a local server that serves a HTTPS error / HTTP redirect
+ server.addResponse(Request(TEST_PORTAL_URL_PATH), Status.OK,
+ content = "Test captive portal content")
+ server.addResponse(Request(TEST_HTTPS_URL_PATH), Status.INTERNAL_ERROR)
+ server.addResponse(Request(TEST_HTTP_URL_PATH), Status.REDIRECT,
+ locationHeader = makeUrl(TEST_PORTAL_URL_PATH))
+ setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH))
+ setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH))
+ // URL expiration needs to be in the next 10 minutes
+ setUrlExpirationDeviceConfig(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(9))
+
+ // Wait for a captive portal to be detected on the network
+ val wifiNetworkFuture = CompletableFuture<Network>()
+ val wifiCb = object : NetworkCallback() {
+ override fun onCapabilitiesChanged(
+ network: Network,
+ nc: NetworkCapabilities
+ ) {
+ if (nc.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) {
+ wifiNetworkFuture.complete(network)
+ }
+ }
+ }
+ cm.requestNetwork(NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(), wifiCb)
+
+ try {
+ reconnectWifi()
+ val network = wifiNetworkFuture.assertGet(WIFI_CONNECT_TIMEOUT_MS,
+ "Captive portal not detected after ${WIFI_CONNECT_TIMEOUT_MS}ms")
+
+ val wifiDefaultMessage = "Wifi should not be the default network when a captive " +
+ "portal was detected and another network (mobile data) can provide internet " +
+ "access."
+ assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
+
+ val startPortalAppPermission =
+ if (isDevSdkInRange(0, Build.VERSION_CODES.Q)) CONNECTIVITY_INTERNAL
+ else NETWORK_SETTINGS
+ runAsShell(startPortalAppPermission) { cm.startCaptivePortalApp(network) }
+
+ // Expect the portal content to be fetched at some point after detecting the portal.
+ // Some implementations may fetch the URL before startCaptivePortalApp is called.
+ assertNotNull(server.requestsRecord.poll(TEST_TIMEOUT_MS, pos = 0) {
+ it.path == TEST_PORTAL_URL_PATH
+ }, "The captive portal login page was still not fetched ${TEST_TIMEOUT_MS}ms " +
+ "after startCaptivePortalApp.")
+
+ assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
+ } finally {
+ cm.unregisterNetworkCallback(wifiCb)
+ server.stop()
+ // disconnectFromCell should be called after connectToCell
+ utils.disconnectFromCell()
+ }
+ }
+
+ /**
+ * Create a URL string that, when fetched, will hit the test server with the given URL [path].
+ */
+ private fun makeUrl(path: String) = "http://localhost:${server.listeningPort}" + path
+
+ private fun reconnectWifi() {
+ utils.ensureWifiDisconnected(null /* wifiNetworkToCheck */)
+ utils.ensureWifiConnected()
+ }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
new file mode 100644
index 0000000..54509cd
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -0,0 +1,576 @@
+/*
+ * Copyright (C) 2020 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 static android.content.pm.PackageManager.FEATURE_TELEPHONY;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_ATTEMPTED_BITMASK;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_SUCCEEDED_BITMASK;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_VALIDATION_RESULT;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.NETWORK_VALIDATION_RESULT_VALID;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_DNS_EVENTS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_TCP_METRICS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE;
+import static android.net.ConnectivityDiagnosticsManager.persistableBundleEquals;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+
+import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityDiagnosticsManager;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.platform.test.annotations.AppModeFull;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.Pair;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.util.ArrayUtils;
+import com.android.net.module.util.ArrayTrackRecord;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.SkipPresubmit;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@IgnoreUpTo(Build.VERSION_CODES.Q) // ConnectivityDiagnosticsManager did not exist in Q
+@AppModeFull(reason = "CHANGE_NETWORK_STATE, MANAGE_TEST_NETWORKS not grantable to instant apps")
+public class ConnectivityDiagnosticsManagerTest {
+ private static final int CALLBACK_TIMEOUT_MILLIS = 5000;
+ private static final int NO_CALLBACK_INVOKED_TIMEOUT = 500;
+ private static final long TIMESTAMP = 123456789L;
+ private static final int DNS_CONSECUTIVE_TIMEOUTS = 5;
+ private static final int COLLECTION_PERIOD_MILLIS = 5000;
+ private static final int FAIL_RATE_PERCENTAGE = 100;
+ private static final int UNKNOWN_DETECTION_METHOD = 4;
+ private static final int FILTERED_UNKNOWN_DETECTION_METHOD = 0;
+ private static final int CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT = 5000;
+ private static final int DELAY_FOR_ADMIN_UIDS_MILLIS = 2000;
+
+ private static final Executor INLINE_EXECUTOR = x -> x.run();
+
+ private static final NetworkRequest TEST_NETWORK_REQUEST =
+ new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_TEST)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .removeCapability(NET_CAPABILITY_NOT_VPN)
+ .build();
+
+ private static final String SHA_256 = "SHA-256";
+
+ private static final NetworkRequest CELLULAR_NETWORK_REQUEST =
+ new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build();
+
+ private static final IBinder BINDER = new Binder();
+
+ private Context mContext;
+ private ConnectivityManager mConnectivityManager;
+ private ConnectivityDiagnosticsManager mCdm;
+ private CarrierConfigManager mCarrierConfigManager;
+ private PackageManager mPackageManager;
+ private TelephonyManager mTelephonyManager;
+
+ // Callback used to keep TestNetworks up when there are no other outstanding NetworkRequests
+ // for it.
+ private TestNetworkCallback mTestNetworkCallback;
+ private Network mTestNetwork;
+ private ParcelFileDescriptor mTestNetworkFD;
+
+ private List<TestConnectivityDiagnosticsCallback> mRegisteredCallbacks;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getContext();
+ mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
+ mCdm = mContext.getSystemService(ConnectivityDiagnosticsManager.class);
+ mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class);
+ mPackageManager = mContext.getPackageManager();
+ mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
+
+ mTestNetworkCallback = new TestNetworkCallback();
+ mConnectivityManager.requestNetwork(TEST_NETWORK_REQUEST, mTestNetworkCallback);
+
+ mRegisteredCallbacks = new ArrayList<>();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mConnectivityManager.unregisterNetworkCallback(mTestNetworkCallback);
+ if (mTestNetwork != null) {
+ runWithShellPermissionIdentity(() -> {
+ final TestNetworkManager tnm = mContext.getSystemService(TestNetworkManager.class);
+ tnm.teardownTestNetwork(mTestNetwork);
+ });
+ mTestNetwork = null;
+ }
+
+ if (mTestNetworkFD != null) {
+ mTestNetworkFD.close();
+ mTestNetworkFD = null;
+ }
+
+ for (TestConnectivityDiagnosticsCallback cb : mRegisteredCallbacks) {
+ mCdm.unregisterConnectivityDiagnosticsCallback(cb);
+ }
+ }
+
+ @Test
+ public void testRegisterConnectivityDiagnosticsCallback() throws Exception {
+ mTestNetworkFD = setUpTestNetwork().getFileDescriptor();
+ mTestNetwork = mTestNetworkCallback.waitForAvailable();
+
+ final TestConnectivityDiagnosticsCallback cb =
+ createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST);
+
+ final String interfaceName =
+ mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName();
+
+ cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName);
+ cb.assertNoCallback();
+ }
+
+ @SkipPresubmit(reason = "Flaky: b/159718782; add to presubmit after fixing")
+ @Test
+ public void testRegisterCallbackWithCarrierPrivileges() throws Exception {
+ assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
+
+ final int subId = SubscriptionManager.getDefaultSubscriptionId();
+ if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ fail("Need an active subscription. Please ensure that the device has working mobile"
+ + " data.");
+ }
+
+ final CarrierConfigReceiver carrierConfigReceiver = new CarrierConfigReceiver(subId);
+ mContext.registerReceiver(
+ carrierConfigReceiver,
+ new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
+
+ final TestNetworkCallback testNetworkCallback = new TestNetworkCallback();
+
+ try {
+ doBroadcastCarrierConfigsAndVerifyOnConnectivityReportAvailable(
+ subId, carrierConfigReceiver, testNetworkCallback);
+ } finally {
+ runWithShellPermissionIdentity(
+ () -> mCarrierConfigManager.overrideConfig(subId, null),
+ android.Manifest.permission.MODIFY_PHONE_STATE);
+ mConnectivityManager.unregisterNetworkCallback(testNetworkCallback);
+ mContext.unregisterReceiver(carrierConfigReceiver);
+ }
+ }
+
+ private String getCertHashForThisPackage() throws Exception {
+ final PackageInfo pkgInfo =
+ mPackageManager.getPackageInfo(
+ mContext.getOpPackageName(), PackageManager.GET_SIGNATURES);
+ final MessageDigest md = MessageDigest.getInstance(SHA_256);
+ final byte[] certHash = md.digest(pkgInfo.signatures[0].toByteArray());
+ return IccUtils.bytesToHexString(certHash);
+ }
+
+ private void doBroadcastCarrierConfigsAndVerifyOnConnectivityReportAvailable(
+ int subId,
+ @NonNull CarrierConfigReceiver carrierConfigReceiver,
+ @NonNull TestNetworkCallback testNetworkCallback)
+ throws Exception {
+ final PersistableBundle carrierConfigs = new PersistableBundle();
+ carrierConfigs.putStringArray(
+ CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
+ new String[] {getCertHashForThisPackage()});
+
+ runWithShellPermissionIdentity(
+ () -> {
+ mCarrierConfigManager.overrideConfig(subId, carrierConfigs);
+ mCarrierConfigManager.notifyConfigChangedForSubId(subId);
+ },
+ android.Manifest.permission.MODIFY_PHONE_STATE);
+
+ // TODO(b/157779832): This should use android.permission.CHANGE_NETWORK_STATE. However, the
+ // shell does not have CHANGE_NETWORK_STATE, so use CONNECTIVITY_INTERNAL until the shell
+ // permissions are updated.
+ runWithShellPermissionIdentity(
+ () -> mConnectivityManager.requestNetwork(
+ CELLULAR_NETWORK_REQUEST, testNetworkCallback),
+ android.Manifest.permission.CONNECTIVITY_INTERNAL);
+
+ final Network network = testNetworkCallback.waitForAvailable();
+ assertNotNull(network);
+
+ assertTrue("Didn't receive broadcast for ACTION_CARRIER_CONFIG_CHANGED for subId=" + subId,
+ carrierConfigReceiver.waitForCarrierConfigChanged());
+ assertTrue("Don't have Carrier Privileges after adding cert for this package",
+ mTelephonyManager.createForSubscriptionId(subId).hasCarrierPrivileges());
+
+ // Wait for CarrierPrivilegesTracker to receive the ACTION_CARRIER_CONFIG_CHANGED
+ // broadcast. CPT then needs to update the corresponding DataConnection, which then
+ // updates ConnectivityService. Unfortunately, this update to the NetworkCapabilities in
+ // CS does not trigger NetworkCallback#onCapabilitiesChanged as changing the
+ // administratorUids is not a publicly visible change. In lieu of a better signal to
+ // detministically wait for, use Thread#sleep here.
+ // TODO(b/157949581): replace this Thread#sleep with a deterministic signal
+ Thread.sleep(DELAY_FOR_ADMIN_UIDS_MILLIS);
+
+ final TestConnectivityDiagnosticsCallback connDiagsCallback =
+ createAndRegisterConnectivityDiagnosticsCallback(CELLULAR_NETWORK_REQUEST);
+
+ final String interfaceName =
+ mConnectivityManager.getLinkProperties(network).getInterfaceName();
+ connDiagsCallback.expectOnConnectivityReportAvailable(
+ network, interfaceName, TRANSPORT_CELLULAR);
+ connDiagsCallback.assertNoCallback();
+ }
+
+ @Test
+ public void testRegisterDuplicateConnectivityDiagnosticsCallback() {
+ final TestConnectivityDiagnosticsCallback cb =
+ createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST);
+
+ try {
+ mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb);
+ fail("Registering the same callback twice should throw an IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void testUnregisterConnectivityDiagnosticsCallback() {
+ final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback();
+ mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb);
+ mCdm.unregisterConnectivityDiagnosticsCallback(cb);
+ }
+
+ @Test
+ public void testUnregisterUnknownConnectivityDiagnosticsCallback() {
+ // Expected to silently ignore the unregister() call
+ mCdm.unregisterConnectivityDiagnosticsCallback(new TestConnectivityDiagnosticsCallback());
+ }
+
+ @Test
+ public void testOnConnectivityReportAvailable() throws Exception {
+ final TestConnectivityDiagnosticsCallback cb =
+ createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST);
+
+ mTestNetworkFD = setUpTestNetwork().getFileDescriptor();
+ mTestNetwork = mTestNetworkCallback.waitForAvailable();
+
+ final String interfaceName =
+ mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName();
+
+ cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName);
+ cb.assertNoCallback();
+ }
+
+ @Test
+ public void testOnDataStallSuspected_DnsEvents() throws Exception {
+ final PersistableBundle extras = new PersistableBundle();
+ extras.putInt(KEY_DNS_CONSECUTIVE_TIMEOUTS, DNS_CONSECUTIVE_TIMEOUTS);
+
+ verifyOnDataStallSuspected(DETECTION_METHOD_DNS_EVENTS, TIMESTAMP, extras);
+ }
+
+ @Test
+ public void testOnDataStallSuspected_TcpMetrics() throws Exception {
+ final PersistableBundle extras = new PersistableBundle();
+ extras.putInt(KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS, COLLECTION_PERIOD_MILLIS);
+ extras.putInt(KEY_TCP_PACKET_FAIL_RATE, FAIL_RATE_PERCENTAGE);
+
+ verifyOnDataStallSuspected(DETECTION_METHOD_TCP_METRICS, TIMESTAMP, extras);
+ }
+
+ @Test
+ public void testOnDataStallSuspected_UnknownDetectionMethod() throws Exception {
+ verifyOnDataStallSuspected(
+ UNKNOWN_DETECTION_METHOD,
+ FILTERED_UNKNOWN_DETECTION_METHOD,
+ TIMESTAMP,
+ PersistableBundle.EMPTY);
+ }
+
+ private void verifyOnDataStallSuspected(
+ int detectionMethod, long timestampMillis, @NonNull PersistableBundle extras)
+ throws Exception {
+ // Input detection method is expected to match received detection method
+ verifyOnDataStallSuspected(detectionMethod, detectionMethod, timestampMillis, extras);
+ }
+
+ private void verifyOnDataStallSuspected(
+ int inputDetectionMethod,
+ int expectedDetectionMethod,
+ long timestampMillis,
+ @NonNull PersistableBundle extras)
+ throws Exception {
+ mTestNetworkFD = setUpTestNetwork().getFileDescriptor();
+ mTestNetwork = mTestNetworkCallback.waitForAvailable();
+
+ final TestConnectivityDiagnosticsCallback cb =
+ createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST);
+
+ final String interfaceName =
+ mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName();
+
+ cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName);
+
+ runWithShellPermissionIdentity(
+ () -> mConnectivityManager.simulateDataStall(
+ inputDetectionMethod, timestampMillis, mTestNetwork, extras),
+ android.Manifest.permission.MANAGE_TEST_NETWORKS);
+
+ cb.expectOnDataStallSuspected(
+ mTestNetwork, interfaceName, expectedDetectionMethod, timestampMillis, extras);
+ cb.assertNoCallback();
+ }
+
+ @Test
+ public void testOnNetworkConnectivityReportedTrue() throws Exception {
+ verifyOnNetworkConnectivityReported(true /* hasConnectivity */);
+ }
+
+ @Test
+ public void testOnNetworkConnectivityReportedFalse() throws Exception {
+ verifyOnNetworkConnectivityReported(false /* hasConnectivity */);
+ }
+
+ private void verifyOnNetworkConnectivityReported(boolean hasConnectivity) throws Exception {
+ mTestNetworkFD = setUpTestNetwork().getFileDescriptor();
+ mTestNetwork = mTestNetworkCallback.waitForAvailable();
+
+ final TestConnectivityDiagnosticsCallback cb =
+ createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST);
+
+ // onConnectivityReportAvailable always invoked when the test network is established
+ final String interfaceName =
+ mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName();
+ cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName);
+ cb.assertNoCallback();
+
+ mConnectivityManager.reportNetworkConnectivity(mTestNetwork, hasConnectivity);
+
+ cb.expectOnNetworkConnectivityReported(mTestNetwork, hasConnectivity);
+
+ // if hasConnectivity does not match the network's known connectivity, it will be
+ // revalidated which will trigger another onConnectivityReportAvailable callback.
+ if (!hasConnectivity) {
+ cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName);
+ }
+
+ cb.assertNoCallback();
+ }
+
+ private TestConnectivityDiagnosticsCallback createAndRegisterConnectivityDiagnosticsCallback(
+ NetworkRequest request) {
+ final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback();
+ mCdm.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, cb);
+ mRegisteredCallbacks.add(cb);
+ return cb;
+ }
+
+ /**
+ * Registers a test NetworkAgent with ConnectivityService with limited capabilities, which leads
+ * to the Network being validated.
+ */
+ @NonNull
+ private TestNetworkInterface setUpTestNetwork() throws Exception {
+ final int[] administratorUids = new int[] {Process.myUid()};
+ return callWithShellPermissionIdentity(
+ () -> {
+ final TestNetworkManager tnm =
+ mContext.getSystemService(TestNetworkManager.class);
+ final TestNetworkInterface tni = tnm.createTunInterface(new LinkAddress[0]);
+ tnm.setupTestNetwork(tni.getInterfaceName(), administratorUids, BINDER);
+ return tni;
+ });
+ }
+
+ private static class TestConnectivityDiagnosticsCallback
+ extends ConnectivityDiagnosticsCallback {
+ private final ArrayTrackRecord<Object>.ReadHead mHistory =
+ new ArrayTrackRecord<Object>().newReadHead();
+
+ @Override
+ public void onConnectivityReportAvailable(ConnectivityReport report) {
+ mHistory.add(report);
+ }
+
+ @Override
+ public void onDataStallSuspected(DataStallReport report) {
+ mHistory.add(report);
+ }
+
+ @Override
+ public void onNetworkConnectivityReported(Network network, boolean hasConnectivity) {
+ mHistory.add(new Pair<Network, Boolean>(network, hasConnectivity));
+ }
+
+ public void expectOnConnectivityReportAvailable(
+ @NonNull Network network, @NonNull String interfaceName) {
+ expectOnConnectivityReportAvailable(network, interfaceName, TRANSPORT_TEST);
+ }
+
+ public void expectOnConnectivityReportAvailable(
+ @NonNull Network network, @NonNull String interfaceName, int transportType) {
+ final ConnectivityReport result =
+ (ConnectivityReport) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true);
+ assertEquals(network, result.getNetwork());
+
+ final NetworkCapabilities nc = result.getNetworkCapabilities();
+ assertNotNull(nc);
+ assertTrue(nc.hasTransport(transportType));
+ assertNotNull(result.getLinkProperties());
+ assertEquals(interfaceName, result.getLinkProperties().getInterfaceName());
+
+ final PersistableBundle extras = result.getAdditionalInfo();
+ assertTrue(extras.containsKey(KEY_NETWORK_VALIDATION_RESULT));
+ final int validationResult = extras.getInt(KEY_NETWORK_VALIDATION_RESULT);
+ assertEquals("Network validation result is not 'valid'",
+ NETWORK_VALIDATION_RESULT_VALID, validationResult);
+
+ assertTrue(extras.containsKey(KEY_NETWORK_PROBES_SUCCEEDED_BITMASK));
+ final int probesSucceeded = extras.getInt(KEY_NETWORK_VALIDATION_RESULT);
+ assertTrue("PROBES_SUCCEEDED mask not in expected range", probesSucceeded >= 0);
+
+ assertTrue(extras.containsKey(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK));
+ final int probesAttempted = extras.getInt(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK);
+ assertTrue("PROBES_ATTEMPTED mask not in expected range", probesAttempted >= 0);
+ }
+
+ public void expectOnDataStallSuspected(
+ @NonNull Network network,
+ @NonNull String interfaceName,
+ int detectionMethod,
+ long timestampMillis,
+ @NonNull PersistableBundle extras) {
+ final DataStallReport result =
+ (DataStallReport) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true);
+ assertEquals(network, result.getNetwork());
+ assertEquals(detectionMethod, result.getDetectionMethod());
+ assertEquals(timestampMillis, result.getReportTimestamp());
+
+ final NetworkCapabilities nc = result.getNetworkCapabilities();
+ assertNotNull(nc);
+ assertTrue(nc.hasTransport(TRANSPORT_TEST));
+ assertNotNull(result.getLinkProperties());
+ assertEquals(interfaceName, result.getLinkProperties().getInterfaceName());
+
+ assertTrue(persistableBundleEquals(extras, result.getStallDetails()));
+ }
+
+ public void expectOnNetworkConnectivityReported(
+ @NonNull Network network, boolean hasConnectivity) {
+ final Pair<Network, Boolean> result =
+ (Pair<Network, Boolean>) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true);
+ assertEquals(network, result.first /* network */);
+ assertEquals(hasConnectivity, result.second /* hasConnectivity */);
+ }
+
+ public void assertNoCallback() {
+ // If no more callbacks exist, there should be nothing left in the ReadHead
+ assertNull("Unexpected event in history",
+ mHistory.poll(NO_CALLBACK_INVOKED_TIMEOUT, x -> true));
+ }
+ }
+
+ private class CarrierConfigReceiver extends BroadcastReceiver {
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+ private final int mSubId;
+
+ CarrierConfigReceiver(int subId) {
+ mSubId = subId;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(intent.getAction())) {
+ return;
+ }
+
+ final int subId =
+ intent.getIntExtra(
+ CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX,
+ SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+ if (mSubId != subId) return;
+
+ final PersistableBundle carrierConfigs = mCarrierConfigManager.getConfigForSubId(subId);
+ if (!CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfigs)) return;
+
+ final String[] certs =
+ carrierConfigs.getStringArray(
+ CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY);
+ try {
+ if (ArrayUtils.contains(certs, getCertHashForThisPackage())) {
+ mLatch.countDown();
+ }
+ } catch (Exception e) {
+ }
+ }
+
+ boolean waitForCarrierConfigChanged() throws Exception {
+ return mLatch.await(CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT, TimeUnit.MILLISECONDS);
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
new file mode 100644
index 0000000..db4e3e7
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -0,0 +1,1530 @@
+/*
+ * Copyright (C) 2009 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 static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.content.pm.PackageManager.FEATURE_ETHERNET;
+import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
+import static android.content.pm.PackageManager.FEATURE_USB_HOST;
+import static android.content.pm.PackageManager.FEATURE_WIFI;
+import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.cts.util.CtsNetUtils.ConnectivityActionReceiver;
+import static android.net.cts.util.CtsNetUtils.HTTP_PORT;
+import static android.net.cts.util.CtsNetUtils.NETWORK_CALLBACK_ACTION;
+import static android.net.cts.util.CtsNetUtils.TEST_HOST;
+import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+import static android.provider.Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.AF_UNSPEC;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.annotation.NonNull;
+import android.app.Instrumentation;
+import android.app.PendingIntent;
+import android.app.UiAutomation;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.IpSecManager;
+import android.net.IpSecManager.UdpEncapsulationSocket;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkConfig;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkInfo.State;
+import android.net.NetworkRequest;
+import android.net.NetworkUtils;
+import android.net.SocketKeepalive;
+import android.net.cts.util.CtsNetUtils;
+import android.net.util.KeepaliveUtils;
+import android.net.wifi.WifiManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.VintfRuntimeInfo;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.testutils.RecorderCallback.CallbackEntry;
+import com.android.testutils.SkipPresubmit;
+import com.android.testutils.TestableNetworkCallback;
+
+import libcore.io.Streams;
+
+import junit.framework.AssertionFailedError;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.Socket;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@RunWith(AndroidJUnit4.class)
+public class ConnectivityManagerTest {
+
+ private static final String TAG = ConnectivityManagerTest.class.getSimpleName();
+
+ public static final int TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE;
+ public static final int TYPE_WIFI = ConnectivityManager.TYPE_WIFI;
+
+ private static final int HOST_ADDRESS = 0x7f000001;// represent ip 127.0.0.1
+ private static final int KEEPALIVE_CALLBACK_TIMEOUT_MS = 2000;
+ private static final int INTERVAL_KEEPALIVE_RETRY_MS = 500;
+ private static final int MAX_KEEPALIVE_RETRY_COUNT = 3;
+ private static final int MIN_KEEPALIVE_INTERVAL = 10;
+
+ // Changing meteredness on wifi involves reconnecting, which can take several seconds (involves
+ // re-associating, DHCP...)
+ private static final int NETWORK_CHANGE_METEREDNESS_TIMEOUT = 30_000;
+ private static final int NUM_TRIES_MULTIPATH_PREF_CHECK = 20;
+ private static final long INTERVAL_MULTIPATH_PREF_CHECK_MS = 500;
+ // device could have only one interface: data, wifi.
+ private static final int MIN_NUM_NETWORK_TYPES = 1;
+
+ // Airplane Mode BroadcastReceiver Timeout
+ private static final long AIRPLANE_MODE_CHANGE_TIMEOUT_MS = 10_000L;
+
+ // Minimum supported keepalive counts for wifi and cellular.
+ public static final int MIN_SUPPORTED_CELLULAR_KEEPALIVE_COUNT = 1;
+ public static final int MIN_SUPPORTED_WIFI_KEEPALIVE_COUNT = 3;
+
+ private static final String NETWORK_METERED_MULTIPATH_PREFERENCE_RES_NAME =
+ "config_networkMeteredMultipathPreference";
+ private static final String KEEPALIVE_ALLOWED_UNPRIVILEGED_RES_NAME =
+ "config_allowedUnprivilegedKeepalivePerUid";
+ private static final String KEEPALIVE_RESERVED_PER_SLOT_RES_NAME =
+ "config_reservedPrivilegedKeepaliveSlots";
+
+ private Context mContext;
+ private Instrumentation mInstrumentation;
+ private ConnectivityManager mCm;
+ private WifiManager mWifiManager;
+ private PackageManager mPackageManager;
+ private final HashMap<Integer, NetworkConfig> mNetworks =
+ new HashMap<Integer, NetworkConfig>();
+ boolean mWifiWasDisabled;
+ private UiAutomation mUiAutomation;
+ private CtsNetUtils mCtsNetUtils;
+
+ @Before
+ public void setUp() throws Exception {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mContext = mInstrumentation.getContext();
+ mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ mPackageManager = mContext.getPackageManager();
+ mCtsNetUtils = new CtsNetUtils(mContext);
+ mWifiWasDisabled = false;
+
+ // Get com.android.internal.R.array.networkAttributes
+ int resId = mContext.getResources().getIdentifier("networkAttributes", "array", "android");
+ String[] naStrings = mContext.getResources().getStringArray(resId);
+ //TODO: What is the "correct" way to determine if this is a wifi only device?
+ boolean wifiOnly = SystemProperties.getBoolean("ro.radio.noril", false);
+ for (String naString : naStrings) {
+ try {
+ NetworkConfig n = new NetworkConfig(naString);
+ if (wifiOnly && ConnectivityManager.isNetworkTypeMobile(n.type)) {
+ continue;
+ }
+ mNetworks.put(n.type, n);
+ } catch (Exception e) {}
+ }
+ mUiAutomation = mInstrumentation.getUiAutomation();
+
+ assertNotNull("CTS requires a working Internet connection", mCm.getActiveNetwork());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ // Return WiFi to its original disabled state after tests that explicitly connect.
+ if (mWifiWasDisabled) {
+ mCtsNetUtils.disconnectFromWifi(null);
+ }
+ if (mCtsNetUtils.cellConnectAttempted()) {
+ mCtsNetUtils.disconnectFromCell();
+ }
+
+ // All tests in this class require a working Internet connection as they start. Make
+ // sure there is still one as they end that's ready to use for the next test to use.
+ final TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.registerDefaultNetworkCallback(callback);
+ try {
+ assertNotNull("Couldn't restore Internet connectivity", callback.waitForAvailable());
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ }
+ }
+
+ /**
+ * Make sure WiFi is connected to an access point if it is not already. If
+ * WiFi is enabled as a result of this function, it will be disabled
+ * automatically in tearDown().
+ */
+ private Network ensureWifiConnected() {
+ mWifiWasDisabled = !mWifiManager.isWifiEnabled();
+ // Even if wifi is enabled, the network may not be connected or ready yet
+ return mCtsNetUtils.connectToWifi();
+ }
+
+ @Test
+ public void testIsNetworkTypeValid() {
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_WIFI));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_MMS));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_SUPL));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_DUN));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_HIPRI));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_WIMAX));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_BLUETOOTH));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_DUMMY));
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_ETHERNET));
+ assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_FOTA));
+ assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_IMS));
+ assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_CBS));
+ assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_WIFI_P2P));
+ assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_IA));
+ assertFalse(mCm.isNetworkTypeValid(-1));
+ assertTrue(mCm.isNetworkTypeValid(0));
+ assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.MAX_NETWORK_TYPE));
+ assertFalse(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.MAX_NETWORK_TYPE+1));
+
+ NetworkInfo[] ni = mCm.getAllNetworkInfo();
+
+ for (NetworkInfo n: ni) {
+ assertTrue(ConnectivityManager.isNetworkTypeValid(n.getType()));
+ }
+
+ }
+
+ @Test
+ public void testSetNetworkPreference() {
+ // getNetworkPreference() and setNetworkPreference() are both deprecated so they do
+ // not preform any action. Verify they are at least still callable.
+ mCm.setNetworkPreference(mCm.getNetworkPreference());
+ }
+
+ @Test
+ public void testGetActiveNetworkInfo() {
+ NetworkInfo ni = mCm.getActiveNetworkInfo();
+
+ assertNotNull("You must have an active network connection to complete CTS", ni);
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ni.getType()));
+ assertTrue(ni.getState() == State.CONNECTED);
+ }
+
+ @Test
+ public void testGetActiveNetwork() {
+ Network network = mCm.getActiveNetwork();
+ assertNotNull("You must have an active network connection to complete CTS", network);
+
+ NetworkInfo ni = mCm.getNetworkInfo(network);
+ assertNotNull("Network returned from getActiveNetwork was invalid", ni);
+
+ // Similar to testGetActiveNetworkInfo above.
+ assertTrue(ConnectivityManager.isNetworkTypeValid(ni.getType()));
+ assertTrue(ni.getState() == State.CONNECTED);
+ }
+
+ @Test
+ public void testGetNetworkInfo() {
+ for (int type = -1; type <= ConnectivityManager.MAX_NETWORK_TYPE+1; type++) {
+ if (shouldBeSupported(type)) {
+ NetworkInfo ni = mCm.getNetworkInfo(type);
+ assertTrue("Info shouldn't be null for " + type, ni != null);
+ State state = ni.getState();
+ assertTrue("Bad state for " + type, State.UNKNOWN.ordinal() >= state.ordinal()
+ && state.ordinal() >= State.CONNECTING.ordinal());
+ DetailedState ds = ni.getDetailedState();
+ assertTrue("Bad detailed state for " + type,
+ DetailedState.FAILED.ordinal() >= ds.ordinal()
+ && ds.ordinal() >= DetailedState.IDLE.ordinal());
+ } else {
+ assertNull("Info should be null for " + type, mCm.getNetworkInfo(type));
+ }
+ }
+ }
+
+ @Test
+ public void testGetAllNetworkInfo() {
+ NetworkInfo[] ni = mCm.getAllNetworkInfo();
+ assertTrue(ni.length >= MIN_NUM_NETWORK_TYPES);
+ for (int type = 0; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) {
+ int desiredFoundCount = (shouldBeSupported(type) ? 1 : 0);
+ int foundCount = 0;
+ for (NetworkInfo i : ni) {
+ if (i.getType() == type) foundCount++;
+ }
+ if (foundCount != desiredFoundCount) {
+ Log.e(TAG, "failure in testGetAllNetworkInfo. Dump of returned NetworkInfos:");
+ for (NetworkInfo networkInfo : ni) Log.e(TAG, " " + networkInfo);
+ }
+ assertTrue("Unexpected foundCount of " + foundCount + " for type " + type,
+ foundCount == desiredFoundCount);
+ }
+ }
+
+ /**
+ * Tests that connections can be opened on WiFi and cellphone networks,
+ * and that they are made from different IP addresses.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ @SkipPresubmit(reason = "Virtual devices use a single internet connection for all networks")
+ public void testOpenConnection() throws Exception {
+ boolean canRunTest = mPackageManager.hasSystemFeature(FEATURE_WIFI)
+ && mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
+ if (!canRunTest) {
+ Log.i(TAG,"testOpenConnection cannot execute unless device supports both WiFi "
+ + "and a cellular connection");
+ return;
+ }
+
+ Network wifiNetwork = mCtsNetUtils.connectToWifi();
+ Network cellNetwork = mCtsNetUtils.connectToCell();
+ // This server returns the requestor's IP address as the response body.
+ URL url = new URL("http://google-ipv6test.appspot.com/ip.js?fmt=text");
+ String wifiAddressString = httpGet(wifiNetwork, url);
+ String cellAddressString = httpGet(cellNetwork, url);
+
+ assertFalse(String.format("Same address '%s' on two different networks (%s, %s)",
+ wifiAddressString, wifiNetwork, cellNetwork),
+ wifiAddressString.equals(cellAddressString));
+
+ // Verify that the IP addresses that the requests appeared to come from are actually on the
+ // respective networks.
+ assertOnNetwork(wifiAddressString, wifiNetwork);
+ assertOnNetwork(cellAddressString, cellNetwork);
+
+ assertFalse("Unexpectedly equal: " + wifiNetwork, wifiNetwork.equals(cellNetwork));
+ }
+
+ /**
+ * Performs a HTTP GET to the specified URL on the specified Network, and returns
+ * the response body decoded as UTF-8.
+ */
+ private static String httpGet(Network network, URL httpUrl) throws IOException {
+ HttpURLConnection connection = (HttpURLConnection) network.openConnection(httpUrl);
+ try {
+ InputStream inputStream = connection.getInputStream();
+ return Streams.readFully(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+ } finally {
+ connection.disconnect();
+ }
+ }
+
+ private void assertOnNetwork(String adressString, Network network) throws UnknownHostException {
+ InetAddress address = InetAddress.getByName(adressString);
+ LinkProperties linkProperties = mCm.getLinkProperties(network);
+ // To make sure that the request went out on the right network, check that
+ // the IP address seen by the server is assigned to the expected network.
+ // We can only do this for IPv6 addresses, because in IPv4 we will likely
+ // have a private IPv4 address, and that won't match what the server sees.
+ if (address instanceof Inet6Address) {
+ assertContains(linkProperties.getAddresses(), address);
+ }
+ }
+
+ private static<T> void assertContains(Collection<T> collection, T element) {
+ assertTrue(element + " not found in " + collection, collection.contains(element));
+ }
+
+ private void assertStartUsingNetworkFeatureUnsupported(int networkType, String feature) {
+ try {
+ mCm.startUsingNetworkFeature(networkType, feature);
+ fail("startUsingNetworkFeature is no longer supported in the current API version");
+ } catch (UnsupportedOperationException expected) {}
+ }
+
+ private void assertStopUsingNetworkFeatureUnsupported(int networkType, String feature) {
+ try {
+ mCm.startUsingNetworkFeature(networkType, feature);
+ fail("stopUsingNetworkFeature is no longer supported in the current API version");
+ } catch (UnsupportedOperationException expected) {}
+ }
+
+ private void assertRequestRouteToHostUnsupported(int networkType, int hostAddress) {
+ try {
+ mCm.requestRouteToHost(networkType, hostAddress);
+ fail("requestRouteToHost is no longer supported in the current API version");
+ } catch (UnsupportedOperationException expected) {}
+ }
+
+ @Test
+ public void testStartUsingNetworkFeature() {
+
+ final String invalidateFeature = "invalidateFeature";
+ final String mmsFeature = "enableMMS";
+
+ assertStartUsingNetworkFeatureUnsupported(TYPE_MOBILE, invalidateFeature);
+ assertStopUsingNetworkFeatureUnsupported(TYPE_MOBILE, invalidateFeature);
+ assertStartUsingNetworkFeatureUnsupported(TYPE_WIFI, mmsFeature);
+ }
+
+ private boolean shouldEthernetBeSupported() {
+ // Instant mode apps aren't allowed to query the Ethernet service due to selinux policies.
+ // When in instant mode, don't fail if the Ethernet service is available. Instead, rely on
+ // the fact that Ethernet should be supported if the device has a hardware Ethernet port, or
+ // if the device can be a USB host and thus can use USB Ethernet adapters.
+ //
+ // Note that this test this will still fail in instant mode if a device supports Ethernet
+ // via other hardware means. We are not currently aware of any such device.
+ return (mContext.getSystemService(Context.ETHERNET_SERVICE) != null) ||
+ mPackageManager.hasSystemFeature(FEATURE_ETHERNET) ||
+ mPackageManager.hasSystemFeature(FEATURE_USB_HOST);
+ }
+
+ private boolean shouldBeSupported(int networkType) {
+ return mNetworks.containsKey(networkType) ||
+ (networkType == ConnectivityManager.TYPE_VPN) ||
+ (networkType == ConnectivityManager.TYPE_ETHERNET && shouldEthernetBeSupported());
+ }
+
+ @Test
+ public void testIsNetworkSupported() {
+ for (int type = -1; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) {
+ boolean supported = mCm.isNetworkSupported(type);
+ if (shouldBeSupported(type)) {
+ assertTrue("Network type " + type + " should be supported", supported);
+ } else {
+ assertFalse("Network type " + type + " should not be supported", supported);
+ }
+ }
+ }
+
+ @Test
+ public void testRequestRouteToHost() {
+ for (int type = -1 ; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) {
+ assertRequestRouteToHostUnsupported(type, HOST_ADDRESS);
+ }
+ }
+
+ @Test
+ public void testTest() {
+ mCm.getBackgroundDataSetting();
+ }
+
+ private NetworkRequest makeWifiNetworkRequest() {
+ return new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .build();
+ }
+
+ private NetworkRequest makeCellNetworkRequest() {
+ return new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .build();
+ }
+
+ /**
+ * Exercises both registerNetworkCallback and unregisterNetworkCallback. This checks to
+ * see if we get a callback for the TRANSPORT_WIFI transport type being available.
+ *
+ * <p>In order to test that a NetworkCallback occurs, we need some change in the network
+ * state (either a transport or capability is now available). The most straightforward is
+ * WiFi. We could add a version that uses the telephony data connection but it's not clear
+ * that it would increase test coverage by much (how many devices have 3G radio but not Wifi?).
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testRegisterNetworkCallback() {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testRegisterNetworkCallback cannot execute unless device supports WiFi");
+ return;
+ }
+
+ // We will register for a WIFI network being available or lost.
+ final TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback);
+
+ final TestNetworkCallback defaultTrackingCallback = new TestNetworkCallback();
+ mCm.registerDefaultNetworkCallback(defaultTrackingCallback);
+
+ Network wifiNetwork = null;
+
+ try {
+ ensureWifiConnected();
+
+ // Now we should expect to get a network callback about availability of the wifi
+ // network even if it was already connected as a state-based action when the callback
+ // is registered.
+ wifiNetwork = callback.waitForAvailable();
+ assertNotNull("Did not receive NetworkCallback.onAvailable for TRANSPORT_WIFI",
+ wifiNetwork);
+
+ assertNotNull("Did not receive NetworkCallback.onAvailable for any default network",
+ defaultTrackingCallback.waitForAvailable());
+ } catch (InterruptedException e) {
+ fail("Broadcast receiver or NetworkCallback wait was interrupted.");
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ mCm.unregisterNetworkCallback(defaultTrackingCallback);
+ }
+ }
+
+ /**
+ * Tests both registerNetworkCallback and unregisterNetworkCallback similarly to
+ * {@link #testRegisterNetworkCallback} except that a {@code PendingIntent} is used instead
+ * of a {@code NetworkCallback}.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testRegisterNetworkCallback_withPendingIntent() {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testRegisterNetworkCallback cannot execute unless device supports WiFi");
+ return;
+ }
+
+ // Create a ConnectivityActionReceiver that has an IntentFilter for our locally defined
+ // action, NETWORK_CALLBACK_ACTION.
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(NETWORK_CALLBACK_ACTION);
+
+ ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
+ mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED);
+ mContext.registerReceiver(receiver, filter);
+
+ // Create a broadcast PendingIntent for NETWORK_CALLBACK_ACTION.
+ Intent intent = new Intent(NETWORK_CALLBACK_ACTION);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(
+ mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+ // We will register for a WIFI network being available or lost.
+ mCm.registerNetworkCallback(makeWifiNetworkRequest(), pendingIntent);
+
+ try {
+ ensureWifiConnected();
+
+ // Now we expect to get the Intent delivered notifying of the availability of the wifi
+ // network even if it was already connected as a state-based action when the callback
+ // is registered.
+ assertTrue("Did not receive expected Intent " + intent + " for TRANSPORT_WIFI",
+ receiver.waitForState());
+ } catch (InterruptedException e) {
+ fail("Broadcast receiver or NetworkCallback wait was interrupted.");
+ } finally {
+ mCm.unregisterNetworkCallback(pendingIntent);
+ pendingIntent.cancel();
+ mContext.unregisterReceiver(receiver);
+ }
+ }
+
+ /**
+ * Exercises the requestNetwork with NetworkCallback API. This checks to
+ * see if we get a callback for an INTERNET request.
+ */
+ @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
+ @Test
+ public void testRequestNetworkCallback() {
+ final TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.requestNetwork(new NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build(), callback);
+
+ try {
+ // Wait to get callback for availability of internet
+ Network internetNetwork = callback.waitForAvailable();
+ assertNotNull("Did not receive NetworkCallback#onAvailable for INTERNET",
+ internetNetwork);
+ } catch (InterruptedException e) {
+ fail("NetworkCallback wait was interrupted.");
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ }
+ }
+
+ /**
+ * Exercises the requestNetwork with NetworkCallback API with timeout - expected to
+ * fail. Use WIFI and switch Wi-Fi off.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testRequestNetworkCallback_onUnavailable() {
+ final boolean previousWifiEnabledState = mWifiManager.isWifiEnabled();
+ if (previousWifiEnabledState) {
+ mCtsNetUtils.ensureWifiDisconnected(null);
+ }
+
+ final TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.requestNetwork(new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build(), callback, 100);
+
+ try {
+ // Wait to get callback for unavailability of requested network
+ assertTrue("Did not receive NetworkCallback#onUnavailable",
+ callback.waitForUnavailable());
+ } catch (InterruptedException e) {
+ fail("NetworkCallback wait was interrupted.");
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ if (previousWifiEnabledState) {
+ mCtsNetUtils.connectToWifi();
+ }
+ }
+ }
+
+ private InetAddress getFirstV4Address(Network network) {
+ LinkProperties linkProperties = mCm.getLinkProperties(network);
+ for (InetAddress address : linkProperties.getAddresses()) {
+ if (address instanceof Inet4Address) {
+ return address;
+ }
+ }
+ return null;
+ }
+
+ /** Verify restricted networks cannot be requested. */
+ @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
+ @Test
+ public void testRestrictedNetworks() {
+ // Verify we can request unrestricted networks:
+ NetworkRequest request = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET).build();
+ NetworkCallback callback = new NetworkCallback();
+ mCm.requestNetwork(request, callback);
+ mCm.unregisterNetworkCallback(callback);
+ // Verify we cannot request restricted networks:
+ request = new NetworkRequest.Builder().addCapability(NET_CAPABILITY_IMS).build();
+ callback = new NetworkCallback();
+ try {
+ mCm.requestNetwork(request, callback);
+ fail("No exception thrown when restricted network requested.");
+ } catch (SecurityException expected) {}
+ }
+
+ // Returns "true", "false" or "none"
+ private String getWifiMeteredStatus(String ssid) throws Exception {
+ // Interestingly giving the SSID as an argument to list wifi-networks
+ // only works iff the network in question has the "false" policy.
+ // Also unfortunately runShellCommand does not pass the command to the interpreter
+ // so it's not possible to | grep the ssid.
+ final String command = "cmd netpolicy list wifi-networks";
+ final String policyString = runShellCommand(mInstrumentation, command);
+
+ final Matcher m = Pattern.compile("^" + ssid + ";(true|false|none)$",
+ Pattern.MULTILINE | Pattern.UNIX_LINES).matcher(policyString);
+ if (!m.find()) {
+ fail("Unexpected format from cmd netpolicy");
+ }
+ return m.group(1);
+ }
+
+ // metered should be "true", "false" or "none"
+ private void setWifiMeteredStatus(String ssid, String metered) throws Exception {
+ final String setCommand = "cmd netpolicy set metered-network " + ssid + " " + metered;
+ runShellCommand(mInstrumentation, setCommand);
+ assertEquals(getWifiMeteredStatus(ssid), metered);
+ }
+
+ private String unquoteSSID(String ssid) {
+ // SSID is returned surrounded by quotes if it can be decoded as UTF-8.
+ // Otherwise it's guaranteed not to start with a quote.
+ if (ssid.charAt(0) == '"') {
+ return ssid.substring(1, ssid.length() - 1);
+ } else {
+ return ssid;
+ }
+ }
+
+ private void waitForActiveNetworkMetered(int targetTransportType, boolean requestedMeteredness)
+ throws Exception {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final NetworkCallback networkCallback = new NetworkCallback() {
+ @Override
+ public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
+ if (!nc.hasTransport(targetTransportType)) return;
+
+ final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED);
+ if (metered == requestedMeteredness) {
+ latch.countDown();
+ }
+ }
+ };
+ // Registering a callback here guarantees onCapabilitiesChanged is called immediately
+ // with the current setting. Therefore, if the setting has already been changed,
+ // this method will return right away, and if not it will wait for the setting to change.
+ mCm.registerDefaultNetworkCallback(networkCallback);
+ if (!latch.await(NETWORK_CHANGE_METEREDNESS_TIMEOUT, TimeUnit.MILLISECONDS)) {
+ fail("Timed out waiting for active network metered status to change to "
+ + requestedMeteredness + " ; network = " + mCm.getActiveNetwork());
+ }
+ mCm.unregisterNetworkCallback(networkCallback);
+ }
+
+ private void assertMultipathPreferenceIsEventually(Network network, int oldValue,
+ int expectedValue) {
+ // Quick check : if oldValue == expectedValue, there is no way to guarantee the test
+ // is not flaky.
+ assertNotSame(oldValue, expectedValue);
+
+ for (int i = 0; i < NUM_TRIES_MULTIPATH_PREF_CHECK; ++i) {
+ final int actualValue = mCm.getMultipathPreference(network);
+ if (actualValue == expectedValue) {
+ return;
+ }
+ if (actualValue != oldValue) {
+ fail("Multipath preference is neither previous (" + oldValue
+ + ") nor expected (" + expectedValue + ")");
+ }
+ SystemClock.sleep(INTERVAL_MULTIPATH_PREF_CHECK_MS);
+ }
+ fail("Timed out waiting for multipath preference to change. expected = "
+ + expectedValue + " ; actual = " + mCm.getMultipathPreference(network));
+ }
+
+ private int getCurrentMeteredMultipathPreference(ContentResolver resolver) {
+ final String rawMeteredPref = Settings.Global.getString(resolver,
+ NETWORK_METERED_MULTIPATH_PREFERENCE);
+ return TextUtils.isEmpty(rawMeteredPref)
+ ? getIntResourceForName(NETWORK_METERED_MULTIPATH_PREFERENCE_RES_NAME)
+ : Integer.parseInt(rawMeteredPref);
+ }
+
+ private int findNextPrefValue(ContentResolver resolver) {
+ // A bit of a nuclear hammer, but race conditions in CTS are bad. To be able to
+ // detect a correct setting value without race conditions, the next pref must
+ // be a valid value (range 0..3) that is different from the old setting of the
+ // metered preference and from the unmetered preference.
+ final int meteredPref = getCurrentMeteredMultipathPreference(resolver);
+ final int unmeteredPref = ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED;
+ if (0 != meteredPref && 0 != unmeteredPref) return 0;
+ if (1 != meteredPref && 1 != unmeteredPref) return 1;
+ return 2;
+ }
+
+ /**
+ * Verify that getMultipathPreference does return appropriate values
+ * for metered and unmetered networks.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testGetMultipathPreference() throws Exception {
+ final ContentResolver resolver = mContext.getContentResolver();
+ ensureWifiConnected();
+ final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
+ final String oldMeteredSetting = getWifiMeteredStatus(ssid);
+ final String oldMeteredMultipathPreference = Settings.Global.getString(
+ resolver, NETWORK_METERED_MULTIPATH_PREFERENCE);
+ try {
+ final int initialMeteredPreference = getCurrentMeteredMultipathPreference(resolver);
+ int newMeteredPreference = findNextPrefValue(resolver);
+ Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE,
+ Integer.toString(newMeteredPreference));
+ setWifiMeteredStatus(ssid, "true");
+ waitForActiveNetworkMetered(TRANSPORT_WIFI, true);
+ // Wifi meterness changes from unmetered to metered will disconnect and reconnect since
+ // R.
+ final Network network = ensureWifiConnected();
+ assertEquals(ssid, unquoteSSID(mWifiManager.getConnectionInfo().getSSID()));
+ assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
+ NET_CAPABILITY_NOT_METERED), false);
+ assertMultipathPreferenceIsEventually(network, initialMeteredPreference,
+ newMeteredPreference);
+
+ final int oldMeteredPreference = newMeteredPreference;
+ newMeteredPreference = findNextPrefValue(resolver);
+ Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE,
+ Integer.toString(newMeteredPreference));
+ assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
+ NET_CAPABILITY_NOT_METERED), false);
+ assertMultipathPreferenceIsEventually(network,
+ oldMeteredPreference, newMeteredPreference);
+
+ setWifiMeteredStatus(ssid, "false");
+ // No disconnect from unmetered to metered.
+ waitForActiveNetworkMetered(TRANSPORT_WIFI, false);
+ assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
+ NET_CAPABILITY_NOT_METERED), true);
+ assertMultipathPreferenceIsEventually(network, newMeteredPreference,
+ ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED);
+ } finally {
+ Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE,
+ oldMeteredMultipathPreference);
+ setWifiMeteredStatus(ssid, oldMeteredSetting);
+ }
+ }
+
+ // TODO: move the following socket keep alive test to dedicated test class.
+ /**
+ * Callback used in tcp keepalive offload that allows caller to wait callback fires.
+ */
+ private static class TestSocketKeepaliveCallback extends SocketKeepalive.Callback {
+ public enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR };
+
+ public static class CallbackValue {
+ public final CallbackType callbackType;
+ public final int error;
+
+ private CallbackValue(final CallbackType type, final int error) {
+ this.callbackType = type;
+ this.error = error;
+ }
+
+ public static class OnStartedCallback extends CallbackValue {
+ OnStartedCallback() { super(CallbackType.ON_STARTED, 0); }
+ }
+
+ public static class OnStoppedCallback extends CallbackValue {
+ OnStoppedCallback() { super(CallbackType.ON_STOPPED, 0); }
+ }
+
+ public static class OnErrorCallback extends CallbackValue {
+ OnErrorCallback(final int error) { super(CallbackType.ON_ERROR, error); }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o.getClass() == this.getClass()
+ && this.callbackType == ((CallbackValue) o).callbackType
+ && this.error == ((CallbackValue) o).error;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s(%s, %d)", getClass().getSimpleName(), callbackType, error);
+ }
+ }
+
+ private final LinkedBlockingQueue<CallbackValue> mCallbacks = new LinkedBlockingQueue<>();
+
+ @Override
+ public void onStarted() {
+ mCallbacks.add(new CallbackValue.OnStartedCallback());
+ }
+
+ @Override
+ public void onStopped() {
+ mCallbacks.add(new CallbackValue.OnStoppedCallback());
+ }
+
+ @Override
+ public void onError(final int error) {
+ mCallbacks.add(new CallbackValue.OnErrorCallback(error));
+ }
+
+ public CallbackValue pollCallback() {
+ try {
+ return mCallbacks.poll(KEEPALIVE_CALLBACK_TIMEOUT_MS,
+ TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ fail("Callback not seen after " + KEEPALIVE_CALLBACK_TIMEOUT_MS + " ms");
+ }
+ return null;
+ }
+ private void expectCallback(CallbackValue expectedCallback) {
+ final CallbackValue actualCallback = pollCallback();
+ assertEquals(expectedCallback, actualCallback);
+ }
+
+ public void expectStarted() {
+ expectCallback(new CallbackValue.OnStartedCallback());
+ }
+
+ public void expectStopped() {
+ expectCallback(new CallbackValue.OnStoppedCallback());
+ }
+
+ public void expectError(int error) {
+ expectCallback(new CallbackValue.OnErrorCallback(error));
+ }
+ }
+
+ private InetAddress getAddrByName(final String hostname, final int family) throws Exception {
+ final InetAddress[] allAddrs = InetAddress.getAllByName(hostname);
+ for (InetAddress addr : allAddrs) {
+ if (family == AF_INET && addr instanceof Inet4Address) return addr;
+
+ if (family == AF_INET6 && addr instanceof Inet6Address) return addr;
+
+ if (family == AF_UNSPEC) return addr;
+ }
+ return null;
+ }
+
+ private Socket getConnectedSocket(final Network network, final String host, final int port,
+ final int family) throws Exception {
+ final Socket s = network.getSocketFactory().createSocket();
+ try {
+ final InetAddress addr = getAddrByName(host, family);
+ if (addr == null) fail("Fail to get destination address for " + family);
+
+ final InetSocketAddress sockAddr = new InetSocketAddress(addr, port);
+ s.connect(sockAddr);
+ } catch (Exception e) {
+ s.close();
+ throw e;
+ }
+ return s;
+ }
+
+ private int getSupportedKeepalivesForNet(@NonNull Network network) throws Exception {
+ final NetworkCapabilities nc = mCm.getNetworkCapabilities(network);
+
+ // Get number of supported concurrent keepalives for testing network.
+ final int[] keepalivesPerTransport = KeepaliveUtils.getSupportedKeepalives(mContext);
+ return KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(
+ keepalivesPerTransport, nc);
+ }
+
+ private static boolean isTcpKeepaliveSupportedByKernel() {
+ final String kVersionString = VintfRuntimeInfo.getKernelVersion();
+ return compareMajorMinorVersion(kVersionString, "4.8") >= 0;
+ }
+
+ private static Pair<Integer, Integer> getVersionFromString(String version) {
+ // Only gets major and minor number of the version string.
+ final Pattern versionPattern = Pattern.compile("^(\\d+)(\\.(\\d+))?.*");
+ final Matcher m = versionPattern.matcher(version);
+ if (m.matches()) {
+ final int major = Integer.parseInt(m.group(1));
+ final int minor = TextUtils.isEmpty(m.group(3)) ? 0 : Integer.parseInt(m.group(3));
+ return new Pair<>(major, minor);
+ } else {
+ return new Pair<>(0, 0);
+ }
+ }
+
+ // TODO: Move to util class.
+ private static int compareMajorMinorVersion(final String s1, final String s2) {
+ final Pair<Integer, Integer> v1 = getVersionFromString(s1);
+ final Pair<Integer, Integer> v2 = getVersionFromString(s2);
+
+ if (v1.first == v2.first) {
+ return Integer.compare(v1.second, v2.second);
+ } else {
+ return Integer.compare(v1.first, v2.first);
+ }
+ }
+
+ /**
+ * Verifies that version string compare logic returns expected result for various cases.
+ * Note that only major and minor number are compared.
+ */
+ @Test
+ public void testMajorMinorVersionCompare() {
+ assertEquals(0, compareMajorMinorVersion("4.8.1", "4.8"));
+ assertEquals(1, compareMajorMinorVersion("4.9", "4.8.1"));
+ assertEquals(1, compareMajorMinorVersion("5.0", "4.8"));
+ assertEquals(1, compareMajorMinorVersion("5", "4.8"));
+ assertEquals(0, compareMajorMinorVersion("5", "5.0"));
+ assertEquals(1, compareMajorMinorVersion("5-beta1", "4.8"));
+ assertEquals(0, compareMajorMinorVersion("4.8.0.0", "4.8"));
+ assertEquals(0, compareMajorMinorVersion("4.8-RC1", "4.8"));
+ assertEquals(0, compareMajorMinorVersion("4.8", "4.8"));
+ assertEquals(-1, compareMajorMinorVersion("3.10", "4.8.0"));
+ assertEquals(-1, compareMajorMinorVersion("4.7.10.10", "4.8"));
+ }
+
+ /**
+ * Verifies that the keepalive API cannot create any keepalive when the maximum number of
+ * keepalives is set to 0.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testKeepaliveWifiUnsupported() throws Exception {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testKeepaliveUnsupported cannot execute unless device"
+ + " supports WiFi");
+ return;
+ }
+
+ final Network network = ensureWifiConnected();
+ if (getSupportedKeepalivesForNet(network) != 0) return;
+ final InetAddress srcAddr = getFirstV4Address(network);
+ assumeTrue("This test requires native IPv4", srcAddr != null);
+
+ runWithShellPermissionIdentity(() -> {
+ assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 1, 0));
+ assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 0, 1));
+ });
+ }
+
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
+ public void testCreateTcpKeepalive() throws Exception {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testCreateTcpKeepalive cannot execute unless device supports WiFi");
+ return;
+ }
+
+ final Network network = ensureWifiConnected();
+ if (getSupportedKeepalivesForNet(network) == 0) return;
+ final InetAddress srcAddr = getFirstV4Address(network);
+ assumeTrue("This test requires native IPv4", srcAddr != null);
+
+ // If kernel < 4.8 then it doesn't support TCP keepalive, but it might still support
+ // NAT-T keepalive. If keepalive limits from resource overlay is not zero, TCP keepalive
+ // needs to be supported except if the kernel doesn't support it.
+ if (!isTcpKeepaliveSupportedByKernel()) {
+ // Verify that the callback result is expected.
+ runWithShellPermissionIdentity(() -> {
+ assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 0, 1));
+ });
+ Log.i(TAG, "testCreateTcpKeepalive is skipped for kernel "
+ + VintfRuntimeInfo.getKernelVersion());
+ return;
+ }
+
+ final byte[] requestBytes = CtsNetUtils.HTTP_REQUEST.getBytes("UTF-8");
+ // So far only ipv4 tcp keepalive offload is supported.
+ // TODO: add test case for ipv6 tcp keepalive offload when it is supported.
+ try (Socket s = getConnectedSocket(network, TEST_HOST, HTTP_PORT, AF_INET)) {
+
+ // Should able to start keep alive offload when socket is idle.
+ final Executor executor = mContext.getMainExecutor();
+ final TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback();
+
+ mUiAutomation.adoptShellPermissionIdentity();
+ try (SocketKeepalive sk = mCm.createSocketKeepalive(network, s, executor, callback)) {
+ sk.start(MIN_KEEPALIVE_INTERVAL);
+ callback.expectStarted();
+
+ // App should not able to write during keepalive offload.
+ final OutputStream out = s.getOutputStream();
+ try {
+ out.write(requestBytes);
+ fail("Should not able to write");
+ } catch (IOException e) { }
+ // App should not able to read during keepalive offload.
+ final InputStream in = s.getInputStream();
+ byte[] responseBytes = new byte[4096];
+ try {
+ in.read(responseBytes);
+ fail("Should not able to read");
+ } catch (IOException e) { }
+
+ // Stop.
+ sk.stop();
+ callback.expectStopped();
+ } finally {
+ mUiAutomation.dropShellPermissionIdentity();
+ }
+
+ // Ensure socket is still connected.
+ assertTrue(s.isConnected());
+ assertFalse(s.isClosed());
+
+ // Let socket be not idle.
+ try {
+ final OutputStream out = s.getOutputStream();
+ out.write(requestBytes);
+ } catch (IOException e) {
+ fail("Failed to write data " + e);
+ }
+ // Make sure response data arrives.
+ final MessageQueue fdHandlerQueue = Looper.getMainLooper().getQueue();
+ final FileDescriptor fd = s.getFileDescriptor$();
+ final CountDownLatch mOnReceiveLatch = new CountDownLatch(1);
+ fdHandlerQueue.addOnFileDescriptorEventListener(fd, EVENT_INPUT, (readyFd, events) -> {
+ mOnReceiveLatch.countDown();
+ return 0; // Unregister listener.
+ });
+ if (!mOnReceiveLatch.await(2, TimeUnit.SECONDS)) {
+ fdHandlerQueue.removeOnFileDescriptorEventListener(fd);
+ fail("Timeout: no response data");
+ }
+
+ // Should get ERROR_SOCKET_NOT_IDLE because there is still data in the receive queue
+ // that has not been read.
+ mUiAutomation.adoptShellPermissionIdentity();
+ try (SocketKeepalive sk = mCm.createSocketKeepalive(network, s, executor, callback)) {
+ sk.start(MIN_KEEPALIVE_INTERVAL);
+ callback.expectError(SocketKeepalive.ERROR_SOCKET_NOT_IDLE);
+ } finally {
+ mUiAutomation.dropShellPermissionIdentity();
+ }
+ }
+ }
+
+ private ArrayList<SocketKeepalive> createConcurrentKeepalivesOfType(
+ int requestCount, @NonNull TestSocketKeepaliveCallback callback,
+ Supplier<SocketKeepalive> kaFactory) {
+ final ArrayList<SocketKeepalive> kalist = new ArrayList<>();
+
+ int remainingRetries = MAX_KEEPALIVE_RETRY_COUNT;
+
+ // Test concurrent keepalives with the given supplier.
+ while (kalist.size() < requestCount) {
+ final SocketKeepalive ka = kaFactory.get();
+ ka.start(MIN_KEEPALIVE_INTERVAL);
+ TestSocketKeepaliveCallback.CallbackValue cv = callback.pollCallback();
+ assertNotNull(cv);
+ if (cv.callbackType == TestSocketKeepaliveCallback.CallbackType.ON_ERROR) {
+ if (kalist.size() == 0 && cv.error == SocketKeepalive.ERROR_UNSUPPORTED) {
+ // Unsupported.
+ break;
+ } else if (cv.error == SocketKeepalive.ERROR_INSUFFICIENT_RESOURCES) {
+ // Limit reached or temporary unavailable due to stopped slot is not yet
+ // released.
+ if (remainingRetries > 0) {
+ SystemClock.sleep(INTERVAL_KEEPALIVE_RETRY_MS);
+ remainingRetries--;
+ continue;
+ }
+ break;
+ }
+ }
+ if (cv.callbackType == TestSocketKeepaliveCallback.CallbackType.ON_STARTED) {
+ kalist.add(ka);
+ } else {
+ fail("Unexpected error when creating " + (kalist.size() + 1) + " "
+ + ka.getClass().getSimpleName() + ": " + cv);
+ }
+ }
+
+ return kalist;
+ }
+
+ private @NonNull ArrayList<SocketKeepalive> createConcurrentNattSocketKeepalives(
+ @NonNull Network network, @NonNull InetAddress srcAddr, int requestCount,
+ @NonNull TestSocketKeepaliveCallback callback) throws Exception {
+
+ final Executor executor = mContext.getMainExecutor();
+
+ // Initialize a real NaT-T socket.
+ final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE);
+ final UdpEncapsulationSocket nattSocket = mIpSec.openUdpEncapsulationSocket();
+ final InetAddress dstAddr = getAddrByName(TEST_HOST, AF_INET);
+ assertNotNull(srcAddr);
+ assertNotNull(dstAddr);
+
+ // Test concurrent Nat-T keepalives.
+ final ArrayList<SocketKeepalive> result = createConcurrentKeepalivesOfType(requestCount,
+ callback, () -> mCm.createSocketKeepalive(network, nattSocket,
+ srcAddr, dstAddr, executor, callback));
+
+ nattSocket.close();
+ return result;
+ }
+
+ private @NonNull ArrayList<SocketKeepalive> createConcurrentTcpSocketKeepalives(
+ @NonNull Network network, int requestCount,
+ @NonNull TestSocketKeepaliveCallback callback) {
+ final Executor executor = mContext.getMainExecutor();
+
+ // Create concurrent TCP keepalives.
+ return createConcurrentKeepalivesOfType(requestCount, callback, () -> {
+ // Assert that TCP connections can be established. The file descriptor of tcp
+ // sockets will be duplicated and kept valid in service side if the keepalives are
+ // successfully started.
+ try (Socket tcpSocket = getConnectedSocket(network, TEST_HOST, HTTP_PORT,
+ AF_INET)) {
+ return mCm.createSocketKeepalive(network, tcpSocket, executor, callback);
+ } catch (Exception e) {
+ fail("Unexpected error when creating TCP socket: " + e);
+ }
+ return null;
+ });
+ }
+
+ /**
+ * Creates concurrent keepalives until the specified counts of each type of keepalives are
+ * reached or the expected error callbacks are received for each type of keepalives.
+ *
+ * @return the total number of keepalives created.
+ */
+ private int createConcurrentSocketKeepalives(
+ @NonNull Network network, @NonNull InetAddress srcAddr, int nattCount, int tcpCount)
+ throws Exception {
+ final ArrayList<SocketKeepalive> kalist = new ArrayList<>();
+ final TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback();
+
+ kalist.addAll(createConcurrentNattSocketKeepalives(network, srcAddr, nattCount, callback));
+ kalist.addAll(createConcurrentTcpSocketKeepalives(network, tcpCount, callback));
+
+ final int ret = kalist.size();
+
+ // Clean up.
+ for (final SocketKeepalive ka : kalist) {
+ ka.stop();
+ callback.expectStopped();
+ }
+ kalist.clear();
+
+ return ret;
+ }
+
+ /**
+ * Verifies that the concurrent keepalive slots meet the minimum requirement, and don't
+ * get leaked after iterations.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
+ public void testSocketKeepaliveLimitWifi() throws Exception {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testSocketKeepaliveLimitWifi cannot execute unless device"
+ + " supports WiFi");
+ return;
+ }
+
+ final Network network = ensureWifiConnected();
+ final int supported = getSupportedKeepalivesForNet(network);
+ if (supported == 0) {
+ return;
+ }
+ final InetAddress srcAddr = getFirstV4Address(network);
+ assumeTrue("This test requires native IPv4", srcAddr != null);
+
+ runWithShellPermissionIdentity(() -> {
+ // Verifies that the supported keepalive slots meet MIN_SUPPORTED_KEEPALIVE_COUNT.
+ assertGreaterOrEqual(supported, MIN_SUPPORTED_WIFI_KEEPALIVE_COUNT);
+
+ // Verifies that Nat-T keepalives can be established.
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
+ supported + 1, 0));
+ // Verifies that keepalives don't get leaked in second round.
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, supported,
+ 0));
+ });
+
+ // If kernel < 4.8 then it doesn't support TCP keepalive, but it might still support
+ // NAT-T keepalive. Test below cases only if TCP keepalive is supported by kernel.
+ if (!isTcpKeepaliveSupportedByKernel()) return;
+
+ runWithShellPermissionIdentity(() -> {
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, 0,
+ supported + 1));
+
+ // Verifies that different types can be established at the same time.
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
+ supported / 2, supported - supported / 2));
+
+ // Verifies that keepalives don't get leaked in second round.
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, 0,
+ supported));
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
+ supported / 2, supported - supported / 2));
+ });
+ }
+
+ /**
+ * Verifies that the concurrent keepalive slots meet the minimum telephony requirement, and
+ * don't get leaked after iterations.
+ */
+ @AppModeFull(reason = "Cannot request network in instant app mode")
+ @Test
+ @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
+ public void testSocketKeepaliveLimitTelephony() throws Exception {
+ if (!mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)) {
+ Log.i(TAG, "testSocketKeepaliveLimitTelephony cannot execute unless device"
+ + " supports telephony");
+ return;
+ }
+
+ final int firstSdk = Build.VERSION.FIRST_SDK_INT;
+ if (firstSdk < Build.VERSION_CODES.Q) {
+ Log.i(TAG, "testSocketKeepaliveLimitTelephony: skip test for devices launching"
+ + " before Q: " + firstSdk);
+ return;
+ }
+
+ final Network network = mCtsNetUtils.connectToCell();
+ final int supported = getSupportedKeepalivesForNet(network);
+ final InetAddress srcAddr = getFirstV4Address(network);
+ assumeTrue("This test requires native IPv4", srcAddr != null);
+
+ runWithShellPermissionIdentity(() -> {
+ // Verifies that the supported keepalive slots meet minimum requirement.
+ assertGreaterOrEqual(supported, MIN_SUPPORTED_CELLULAR_KEEPALIVE_COUNT);
+ // Verifies that Nat-T keepalives can be established.
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
+ supported + 1, 0));
+ // Verifies that keepalives don't get leaked in second round.
+ assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, supported,
+ 0));
+ });
+ }
+
+ private int getIntResourceForName(@NonNull String resName) {
+ final Resources r = mContext.getResources();
+ final int resId = r.getIdentifier(resName, "integer", "android");
+ return r.getInteger(resId);
+ }
+
+ /**
+ * Verifies that the keepalive slots are limited as customized for unprivileged requests.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
+ public void testSocketKeepaliveUnprivileged() throws Exception {
+ if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
+ Log.i(TAG, "testSocketKeepaliveUnprivileged cannot execute unless device"
+ + " supports WiFi");
+ return;
+ }
+
+ final Network network = ensureWifiConnected();
+ final int supported = getSupportedKeepalivesForNet(network);
+ if (supported == 0) {
+ return;
+ }
+ final InetAddress srcAddr = getFirstV4Address(network);
+ assumeTrue("This test requires native IPv4", srcAddr != null);
+
+ // Resource ID might be shifted on devices that compiled with different symbols.
+ // Thus, resolve ID at runtime is needed.
+ final int allowedUnprivilegedPerUid =
+ getIntResourceForName(KEEPALIVE_ALLOWED_UNPRIVILEGED_RES_NAME);
+ final int reservedPrivilegedSlots =
+ getIntResourceForName(KEEPALIVE_RESERVED_PER_SLOT_RES_NAME);
+ // Verifies that unprivileged request per uid cannot exceed the limit customized in the
+ // resource. Currently, unprivileged keepalive slots are limited to Nat-T only, this test
+ // does not apply to TCP.
+ assertGreaterOrEqual(supported, reservedPrivilegedSlots);
+ assertGreaterOrEqual(supported, allowedUnprivilegedPerUid);
+ final int expectedUnprivileged =
+ Math.min(allowedUnprivilegedPerUid, supported - reservedPrivilegedSlots);
+ assertEquals(expectedUnprivileged,
+ createConcurrentSocketKeepalives(network, srcAddr, supported + 1, 0));
+ }
+
+ private static void assertGreaterOrEqual(long greater, long lesser) {
+ assertTrue("" + greater + " expected to be greater than or equal to " + lesser,
+ greater >= lesser);
+ }
+
+ /**
+ * Verifies that apps are not allowed to access restricted networks even if they declare the
+ * CONNECTIVITY_USE_RESTRICTED_NETWORKS permission in their manifests.
+ * See. b/144679405.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testRestrictedNetworkPermission() throws Exception {
+ // Ensure that CONNECTIVITY_USE_RESTRICTED_NETWORKS isn't granted to this package.
+ final PackageInfo app = mPackageManager.getPackageInfo(mContext.getPackageName(),
+ GET_PERMISSIONS);
+ final int index = ArrayUtils.indexOf(
+ app.requestedPermissions, CONNECTIVITY_USE_RESTRICTED_NETWORKS);
+ assertTrue(index >= 0);
+ assertTrue(app.requestedPermissionsFlags[index] != PERMISSION_GRANTED);
+
+ // Ensure that NetworkUtils.queryUserAccess always returns false since this package should
+ // not have netd system permission to call this function.
+ final Network wifiNetwork = ensureWifiConnected();
+ assertFalse(NetworkUtils.queryUserAccess(Binder.getCallingUid(), wifiNetwork.netId));
+
+ // Ensure that this package cannot bind to any restricted network that's currently
+ // connected.
+ Network[] networks = mCm.getAllNetworks();
+ for (Network network : networks) {
+ NetworkCapabilities nc = mCm.getNetworkCapabilities(network);
+ if (nc != null && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
+ try {
+ network.bindSocket(new Socket());
+ fail("Bind to restricted network " + network + " unexpectedly succeeded");
+ } catch (IOException expected) {}
+ }
+ }
+ }
+
+ /**
+ * Verifies that apps are allowed to call setAirplaneMode if they declare
+ * NETWORK_AIRPLANE_MODE permission in their manifests.
+ * See b/145164696.
+ */
+ @AppModeFull(reason = "NETWORK_AIRPLANE_MODE permission can't be granted to instant apps")
+ @Test
+ public void testSetAirplaneMode() throws Exception{
+ final boolean supportWifi = mPackageManager.hasSystemFeature(FEATURE_WIFI);
+ final boolean supportTelephony = mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
+ // store the current state of airplane mode
+ final boolean isAirplaneModeEnabled = isAirplaneModeEnabled();
+ final TestableNetworkCallback wifiCb = new TestableNetworkCallback();
+ final TestableNetworkCallback telephonyCb = new TestableNetworkCallback();
+ // disable airplane mode to reach a known state
+ runShellCommand("cmd connectivity airplane-mode disable");
+ // Verify that networks are available as expected if wifi or cell is supported. Continue the
+ // test if none of them are supported since test should still able to verify the permission
+ // mechanism.
+ if (supportWifi) requestAndWaitForAvailable(makeWifiNetworkRequest(), wifiCb);
+ if (supportTelephony) requestAndWaitForAvailable(makeCellNetworkRequest(), telephonyCb);
+
+ try {
+ // Verify we cannot set Airplane Mode without correct permission:
+ try {
+ setAndVerifyAirplaneMode(true);
+ fail("SecurityException should have been thrown when setAirplaneMode was called"
+ + "without holding permission NETWORK_AIRPLANE_MODE.");
+ } catch (SecurityException expected) {}
+
+ // disable airplane mode again to reach a known state
+ runShellCommand("cmd connectivity airplane-mode disable");
+
+ // adopt shell permission which holds NETWORK_AIRPLANE_MODE
+ mUiAutomation.adoptShellPermissionIdentity();
+
+ // Verify we can enable Airplane Mode with correct permission:
+ try {
+ setAndVerifyAirplaneMode(true);
+ } catch (SecurityException e) {
+ fail("SecurityException should not have been thrown when setAirplaneMode(true) was"
+ + "called whilst holding the NETWORK_AIRPLANE_MODE permission.");
+ }
+ // Verify that the enabling airplane mode takes effect as expected to prevent flakiness
+ // caused by fast airplane mode switches. Ensure network lost before turning off
+ // airplane mode.
+ if (supportWifi) waitForLost(wifiCb);
+ if (supportTelephony) waitForLost(telephonyCb);
+
+ // Verify we can disable Airplane Mode with correct permission:
+ try {
+ setAndVerifyAirplaneMode(false);
+ } catch (SecurityException e) {
+ fail("SecurityException should not have been thrown when setAirplaneMode(false) was"
+ + "called whilst holding the NETWORK_AIRPLANE_MODE permission.");
+ }
+ // Verify that turning airplane mode off takes effect as expected.
+ if (supportWifi) waitForAvailable(wifiCb);
+ if (supportTelephony) waitForAvailable(telephonyCb);
+ } finally {
+ if (supportWifi) mCm.unregisterNetworkCallback(wifiCb);
+ if (supportTelephony) mCm.unregisterNetworkCallback(telephonyCb);
+ // Restore the previous state of airplane mode and permissions:
+ runShellCommand("cmd connectivity airplane-mode "
+ + (isAirplaneModeEnabled ? "enable" : "disable"));
+ mUiAutomation.dropShellPermissionIdentity();
+ }
+ }
+
+ private void requestAndWaitForAvailable(@NonNull final NetworkRequest request,
+ @NonNull final TestableNetworkCallback cb) {
+ mCm.registerNetworkCallback(request, cb);
+ waitForAvailable(cb);
+ }
+
+ private void waitForAvailable(@NonNull final TestableNetworkCallback cb) {
+ cb.eventuallyExpect(CallbackEntry.AVAILABLE, AIRPLANE_MODE_CHANGE_TIMEOUT_MS,
+ c -> c instanceof CallbackEntry.Available);
+ }
+
+ private void waitForLost(@NonNull final TestableNetworkCallback cb) {
+ cb.eventuallyExpect(CallbackEntry.LOST, AIRPLANE_MODE_CHANGE_TIMEOUT_MS,
+ c -> c instanceof CallbackEntry.Lost);
+ }
+
+ private void setAndVerifyAirplaneMode(Boolean expectedResult)
+ throws Exception {
+ final CompletableFuture<Boolean> actualResult = new CompletableFuture();
+ BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // The defaultValue of getExtraBoolean should be the opposite of what is
+ // expected, thus ensuring a test failure if the extra is absent.
+ actualResult.complete(intent.getBooleanExtra("state", !expectedResult));
+ }
+ };
+ try {
+ mContext.registerReceiver(receiver,
+ new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
+ mCm.setAirplaneMode(expectedResult);
+ final String msg = "Setting Airplane Mode failed,";
+ assertEquals(msg, expectedResult, actualResult.get(AIRPLANE_MODE_CHANGE_TIMEOUT_MS,
+ TimeUnit.MILLISECONDS));
+ } finally {
+ mContext.unregisterReceiver(receiver);
+ }
+ }
+
+ private static boolean isAirplaneModeEnabled() {
+ return runShellCommand("cmd connectivity airplane-mode")
+ .trim().equals("enabled");
+ }
+
+ @Test
+ public void testGetCaptivePortalServerUrl() {
+ final String url = runAsShell(NETWORK_SETTINGS, mCm::getCaptivePortalServerUrl);
+ assertNotNull("getCaptivePortalServerUrl must not be null", url);
+ try {
+ final URL parsedUrl = new URL(url);
+ // As per the javadoc, the URL must be HTTP
+ assertEquals("Invalid captive portal URL protocol", "http", parsedUrl.getProtocol());
+ } catch (MalformedURLException e) {
+ throw new AssertionFailedError("Captive portal server URL is invalid: " + e);
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/CredentialsTest.java b/tests/cts/net/src/android/net/cts/CredentialsTest.java
new file mode 100644
index 0000000..91c3621
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/CredentialsTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2008 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.Credentials;
+import android.test.AndroidTestCase;
+
+public class CredentialsTest extends AndroidTestCase {
+
+ public void testCredentials() {
+ // new the Credentials instance
+ // Test with zero inputs
+ Credentials cred = new Credentials(0, 0, 0);
+ assertEquals(0, cred.getGid());
+ assertEquals(0, cred.getPid());
+ assertEquals(0, cred.getUid());
+
+ // Test with big integer
+ cred = new Credentials(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
+ assertEquals(Integer.MAX_VALUE, cred.getGid());
+ assertEquals(Integer.MAX_VALUE, cred.getPid());
+ assertEquals(Integer.MAX_VALUE, cred.getUid());
+
+ // Test with big negative integer
+ cred = new Credentials(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
+ assertEquals(Integer.MIN_VALUE, cred.getGid());
+ assertEquals(Integer.MIN_VALUE, cred.getPid());
+ assertEquals(Integer.MIN_VALUE, cred.getUid());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
new file mode 100644
index 0000000..4acbbcf
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -0,0 +1,756 @@
+/*
+ * Copyright (C) 2019 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 static android.net.DnsResolver.CLASS_IN;
+import static android.net.DnsResolver.FLAG_EMPTY;
+import static android.net.DnsResolver.FLAG_NO_CACHE_LOOKUP;
+import static android.net.DnsResolver.TYPE_A;
+import static android.net.DnsResolver.TYPE_AAAA;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.system.OsConstants.ETIMEDOUT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.ContentResolver;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.DnsResolver;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.ParseException;
+import android.net.cts.util.CtsNetUtils;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.Looper;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.system.ErrnoException;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import com.android.net.module.util.DnsPacket;
+import com.android.testutils.SkipPresubmit;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
+public class DnsResolverTest extends AndroidTestCase {
+ private static final String TAG = "DnsResolverTest";
+ private static final char[] HEX_CHARS = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+ };
+
+ static final String TEST_DOMAIN = "www.google.com";
+ static final String TEST_NX_DOMAIN = "test1-nx.metric.gstatic.com";
+ static final String INVALID_PRIVATE_DNS_SERVER = "invalid.google";
+ static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
+ static final byte[] TEST_BLOB = new byte[]{
+ /* Header */
+ 0x55, 0x66, /* Transaction ID */
+ 0x01, 0x00, /* Flags */
+ 0x00, 0x01, /* Questions */
+ 0x00, 0x00, /* Answer RRs */
+ 0x00, 0x00, /* Authority RRs */
+ 0x00, 0x00, /* Additional RRs */
+ /* Queries */
+ 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+ 0x00, 0x01, /* Type */
+ 0x00, 0x01 /* Class */
+ };
+ static final int TIMEOUT_MS = 12_000;
+ static final int CANCEL_TIMEOUT_MS = 3_000;
+ static final int CANCEL_RETRY_TIMES = 5;
+ static final int QUERY_TIMES = 10;
+ static final int NXDOMAIN = 3;
+
+ private ContentResolver mCR;
+ private ConnectivityManager mCM;
+ private CtsNetUtils mCtsNetUtils;
+ private Executor mExecutor;
+ private Executor mExecutorInline;
+ private DnsResolver mDns;
+
+ private String mOldMode;
+ private String mOldDnsSpecifier;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mCM = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ mDns = DnsResolver.getInstance();
+ mExecutor = new Handler(Looper.getMainLooper())::post;
+ mExecutorInline = (Runnable r) -> r.run();
+ mCR = getContext().getContentResolver();
+ mCtsNetUtils = new CtsNetUtils(getContext());
+ mCtsNetUtils.storePrivateDnsSetting();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mCtsNetUtils.restorePrivateDnsSetting();
+ super.tearDown();
+ }
+
+ private static String byteArrayToHexString(byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ for (int i = 0; i < bytes.length; ++i) {
+ int b = bytes[i] & 0xFF;
+ hexChars[i * 2] = HEX_CHARS[b >>> 4];
+ hexChars[i * 2 + 1] = HEX_CHARS[b & 0x0F];
+ }
+ return new String(hexChars);
+ }
+
+ private Network[] getTestableNetworks() {
+ final ArrayList<Network> testableNetworks = new ArrayList<Network>();
+ for (Network network : mCM.getAllNetworks()) {
+ final NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+ if (nc != null
+ && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
+ testableNetworks.add(network);
+ }
+ }
+
+ assertTrue(
+ "This test requires that at least one network be connected. " +
+ "Please ensure that the device is connected to a network.",
+ testableNetworks.size() >= 1);
+ // In order to test query with null network, add null as an element.
+ // Test cases which query with null network will go on default network.
+ testableNetworks.add(null);
+ return testableNetworks.toArray(new Network[0]);
+ }
+
+ static private void assertGreaterThan(String msg, int first, int second) {
+ assertTrue(msg + " Excepted " + first + " to be greater than " + second, first > second);
+ }
+
+ private static class DnsParseException extends Exception {
+ public DnsParseException(String msg) {
+ super(msg);
+ }
+ }
+
+ private static class DnsAnswer extends DnsPacket {
+ DnsAnswer(@NonNull byte[] data) throws DnsParseException {
+ super(data);
+
+ // Check QR field.(query (0), or a response (1)).
+ if ((mHeader.flags & (1 << 15)) == 0) {
+ throw new DnsParseException("Not an answer packet");
+ }
+ }
+
+ int getRcode() {
+ return mHeader.rcode;
+ }
+
+ int getANCount() {
+ return mHeader.getRecordCount(ANSECTION);
+ }
+
+ int getQDCount() {
+ return mHeader.getRecordCount(QDSECTION);
+ }
+ }
+
+ /**
+ * A query callback that ensures that the query is cancelled and that onAnswer is never
+ * called. If the query succeeds before it is cancelled, needRetry will return true so the
+ * test can retry.
+ */
+ class VerifyCancelCallback implements DnsResolver.Callback<byte[]> {
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+ private final String mMsg;
+ private final CancellationSignal mCancelSignal;
+ private int mRcode;
+ private DnsAnswer mDnsAnswer;
+ private String mErrorMsg = null;
+
+ VerifyCancelCallback(@NonNull String msg, @Nullable CancellationSignal cancel) {
+ mMsg = msg;
+ mCancelSignal = cancel;
+ }
+
+ VerifyCancelCallback(@NonNull String msg) {
+ this(msg, null);
+ }
+
+ public boolean waitForAnswer(int timeout) throws InterruptedException {
+ return mLatch.await(timeout, TimeUnit.MILLISECONDS);
+ }
+
+ public boolean waitForAnswer() throws InterruptedException {
+ return waitForAnswer(TIMEOUT_MS);
+ }
+
+ public boolean needRetry() throws InterruptedException {
+ return mLatch.await(CANCEL_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void onAnswer(@NonNull byte[] answer, int rcode) {
+ if (mCancelSignal != null && mCancelSignal.isCanceled()) {
+ mErrorMsg = mMsg + " should not have returned any answers";
+ mLatch.countDown();
+ return;
+ }
+
+ mRcode = rcode;
+ try {
+ mDnsAnswer = new DnsAnswer(answer);
+ } catch (ParseException | DnsParseException e) {
+ mErrorMsg = mMsg + e.getMessage();
+ mLatch.countDown();
+ return;
+ }
+ Log.d(TAG, "Reported blob: " + byteArrayToHexString(answer));
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onError(@NonNull DnsResolver.DnsException error) {
+ mErrorMsg = mMsg + error.getMessage();
+ mLatch.countDown();
+ }
+
+ private void assertValidAnswer() {
+ assertNull(mErrorMsg);
+ assertNotNull(mMsg + " No valid answer", mDnsAnswer);
+ assertEquals(mMsg + " Unexpected error: reported rcode" + mRcode +
+ " blob's rcode " + mDnsAnswer.getRcode(), mRcode, mDnsAnswer.getRcode());
+ }
+
+ public void assertHasAnswer() {
+ assertValidAnswer();
+ // Check rcode field.(0, No error condition).
+ assertEquals(mMsg + " Response error, rcode: " + mRcode, mRcode, 0);
+ // Check answer counts.
+ assertGreaterThan(mMsg + " No answer found", mDnsAnswer.getANCount(), 0);
+ // Check question counts.
+ assertGreaterThan(mMsg + " No question found", mDnsAnswer.getQDCount(), 0);
+ }
+
+ public void assertNXDomain() {
+ assertValidAnswer();
+ // Check rcode field.(3, NXDomain).
+ assertEquals(mMsg + " Unexpected rcode: " + mRcode, mRcode, NXDOMAIN);
+ // Check answer counts. Expect 0 answer.
+ assertEquals(mMsg + " Not an empty answer", mDnsAnswer.getANCount(), 0);
+ // Check question counts.
+ assertGreaterThan(mMsg + " No question found", mDnsAnswer.getQDCount(), 0);
+ }
+
+ public void assertEmptyAnswer() {
+ assertValidAnswer();
+ // Check rcode field.(0, No error condition).
+ assertEquals(mMsg + " Response error, rcode: " + mRcode, mRcode, 0);
+ // Check answer counts. Expect 0 answer.
+ assertEquals(mMsg + " Not an empty answer", mDnsAnswer.getANCount(), 0);
+ // Check question counts.
+ assertGreaterThan(mMsg + " No question found", mDnsAnswer.getQDCount(), 0);
+ }
+ }
+
+ public void testRawQuery() throws Exception {
+ doTestRawQuery(mExecutor);
+ }
+
+ public void testRawQueryInline() throws Exception {
+ doTestRawQuery(mExecutorInline);
+ }
+
+ public void testRawQueryBlob() throws Exception {
+ doTestRawQueryBlob(mExecutor);
+ }
+
+ public void testRawQueryBlobInline() throws Exception {
+ doTestRawQueryBlob(mExecutorInline);
+ }
+
+ public void testRawQueryRoot() throws Exception {
+ doTestRawQueryRoot(mExecutor);
+ }
+
+ public void testRawQueryRootInline() throws Exception {
+ doTestRawQueryRoot(mExecutorInline);
+ }
+
+ public void testRawQueryNXDomain() throws Exception {
+ doTestRawQueryNXDomain(mExecutor);
+ }
+
+ public void testRawQueryNXDomainInline() throws Exception {
+ doTestRawQueryNXDomain(mExecutorInline);
+ }
+
+ public void testRawQueryNXDomainWithPrivateDns() throws Exception {
+ doTestRawQueryNXDomainWithPrivateDns(mExecutor);
+ }
+
+ public void testRawQueryNXDomainInlineWithPrivateDns() throws Exception {
+ doTestRawQueryNXDomainWithPrivateDns(mExecutorInline);
+ }
+
+ public void doTestRawQuery(Executor executor) throws InterruptedException {
+ final String msg = "RawQuery " + TEST_DOMAIN;
+ for (Network network : getTestableNetworks()) {
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
+ mDns.rawQuery(network, TEST_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
+ executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertHasAnswer();
+ }
+ }
+
+ public void doTestRawQueryBlob(Executor executor) throws InterruptedException {
+ final byte[] blob = new byte[]{
+ /* Header */
+ 0x55, 0x66, /* Transaction ID */
+ 0x01, 0x00, /* Flags */
+ 0x00, 0x01, /* Questions */
+ 0x00, 0x00, /* Answer RRs */
+ 0x00, 0x00, /* Authority RRs */
+ 0x00, 0x00, /* Additional RRs */
+ /* Queries */
+ 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+ 0x00, 0x01, /* Type */
+ 0x00, 0x01 /* Class */
+ };
+ final String msg = "RawQuery blob " + byteArrayToHexString(blob);
+ for (Network network : getTestableNetworks()) {
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
+ mDns.rawQuery(network, blob, FLAG_NO_CACHE_LOOKUP, executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertHasAnswer();
+ }
+ }
+
+ public void doTestRawQueryRoot(Executor executor) throws InterruptedException {
+ final String dname = "";
+ final String msg = "RawQuery empty dname(ROOT) ";
+ for (Network network : getTestableNetworks()) {
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
+ mDns.rawQuery(network, dname, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
+ executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ // Except no answer record because the root does not have AAAA records.
+ callback.assertEmptyAnswer();
+ }
+ }
+
+ public void doTestRawQueryNXDomain(Executor executor) throws InterruptedException {
+ final String msg = "RawQuery " + TEST_NX_DOMAIN;
+
+ for (Network network : getTestableNetworks()) {
+ final NetworkCapabilities nc = (network != null)
+ ? mCM.getNetworkCapabilities(network)
+ : mCM.getNetworkCapabilities(mCM.getActiveNetwork());
+ assertNotNull("Couldn't determine NetworkCapabilities for " + network, nc);
+ // Some cellular networks configure their DNS servers never to return NXDOMAIN, so don't
+ // test NXDOMAIN on these DNS servers.
+ // b/144521720
+ if (nc.hasTransport(TRANSPORT_CELLULAR)) continue;
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
+ mDns.rawQuery(network, TEST_NX_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
+ executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertNXDomain();
+ }
+ }
+
+ public void doTestRawQueryNXDomainWithPrivateDns(Executor executor)
+ throws InterruptedException {
+ final String msg = "RawQuery " + TEST_NX_DOMAIN + " with private DNS";
+ // Enable private DNS strict mode and set server to dns.google before doing NxDomain test.
+ // b/144521720
+ mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
+ for (Network network : getTestableNetworks()) {
+ final Network networkForPrivateDns =
+ (network != null) ? network : mCM.getActiveNetwork();
+ assertNotNull("Can't find network to await private DNS on", networkForPrivateDns);
+ mCtsNetUtils.awaitPrivateDnsSetting(msg + " wait private DNS setting timeout",
+ networkForPrivateDns, GOOGLE_PRIVATE_DNS_SERVER, true);
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
+ mDns.rawQuery(network, TEST_NX_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
+ executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertNXDomain();
+ }
+ }
+
+ public void testRawQueryCancel() throws InterruptedException {
+ final String msg = "Test cancel RawQuery " + TEST_DOMAIN;
+ // Start a DNS query and the cancel it immediately. Use VerifyCancelCallback to expect
+ // that the query is cancelled before it succeeds. If it is not cancelled before it
+ // succeeds, retry the test until it is.
+ for (Network network : getTestableNetworks()) {
+ boolean retry = false;
+ int round = 0;
+ do {
+ if (++round > CANCEL_RETRY_TIMES) {
+ fail(msg + " cancel failed " + CANCEL_RETRY_TIMES + " times");
+ }
+ final CountDownLatch latch = new CountDownLatch(1);
+ final CancellationSignal cancelSignal = new CancellationSignal();
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg, cancelSignal);
+ mDns.rawQuery(network, TEST_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_EMPTY,
+ mExecutor, cancelSignal, callback);
+ mExecutor.execute(() -> {
+ cancelSignal.cancel();
+ latch.countDown();
+ });
+
+ retry = callback.needRetry();
+ assertTrue(msg + " query was not cancelled",
+ latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } while (retry);
+ }
+ }
+
+ public void testRawQueryBlobCancel() throws InterruptedException {
+ final String msg = "Test cancel RawQuery blob " + byteArrayToHexString(TEST_BLOB);
+ // Start a DNS query and the cancel it immediately. Use VerifyCancelCallback to expect
+ // that the query is cancelled before it succeeds. If it is not cancelled before it
+ // succeeds, retry the test until it is.
+ for (Network network : getTestableNetworks()) {
+ boolean retry = false;
+ int round = 0;
+ do {
+ if (++round > CANCEL_RETRY_TIMES) {
+ fail(msg + " cancel failed " + CANCEL_RETRY_TIMES + " times");
+ }
+ final CountDownLatch latch = new CountDownLatch(1);
+ final CancellationSignal cancelSignal = new CancellationSignal();
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg, cancelSignal);
+ mDns.rawQuery(network, TEST_BLOB, FLAG_EMPTY, mExecutor, cancelSignal, callback);
+ mExecutor.execute(() -> {
+ cancelSignal.cancel();
+ latch.countDown();
+ });
+
+ retry = callback.needRetry();
+ assertTrue(msg + " cancel is not done",
+ latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } while (retry);
+ }
+ }
+
+ public void testCancelBeforeQuery() throws InterruptedException {
+ final String msg = "Test cancelled RawQuery " + TEST_DOMAIN;
+ for (Network network : getTestableNetworks()) {
+ final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
+ final CancellationSignal cancelSignal = new CancellationSignal();
+ cancelSignal.cancel();
+ mDns.rawQuery(network, TEST_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_EMPTY,
+ mExecutor, cancelSignal, callback);
+
+ assertTrue(msg + " should not return any answers",
+ !callback.waitForAnswer(CANCEL_TIMEOUT_MS));
+ }
+ }
+
+ /**
+ * A query callback for InetAddress that ensures that the query is
+ * cancelled and that onAnswer is never called. If the query succeeds
+ * before it is cancelled, needRetry will return true so the
+ * test can retry.
+ */
+ class VerifyCancelInetAddressCallback implements DnsResolver.Callback<List<InetAddress>> {
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+ private final String mMsg;
+ private final List<InetAddress> mAnswers;
+ private final CancellationSignal mCancelSignal;
+ private String mErrorMsg = null;
+
+ VerifyCancelInetAddressCallback(@NonNull String msg, @Nullable CancellationSignal cancel) {
+ this.mMsg = msg;
+ this.mCancelSignal = cancel;
+ mAnswers = new ArrayList<>();
+ }
+
+ public boolean waitForAnswer() throws InterruptedException {
+ return mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ }
+
+ public boolean needRetry() throws InterruptedException {
+ return mLatch.await(CANCEL_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ }
+
+ public boolean isAnswerEmpty() {
+ return mAnswers.isEmpty();
+ }
+
+ public boolean hasIpv6Answer() {
+ for (InetAddress answer : mAnswers) {
+ if (answer instanceof Inet6Address) return true;
+ }
+ return false;
+ }
+
+ public boolean hasIpv4Answer() {
+ for (InetAddress answer : mAnswers) {
+ if (answer instanceof Inet4Address) return true;
+ }
+ return false;
+ }
+
+ public void assertNoError() {
+ assertNull(mErrorMsg);
+ }
+
+ @Override
+ public void onAnswer(@NonNull List<InetAddress> answerList, int rcode) {
+ if (mCancelSignal != null && mCancelSignal.isCanceled()) {
+ mErrorMsg = mMsg + " should not have returned any answers";
+ mLatch.countDown();
+ return;
+ }
+ for (InetAddress addr : answerList) {
+ Log.d(TAG, "Reported addr: " + addr.toString());
+ }
+ mAnswers.clear();
+ mAnswers.addAll(answerList);
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onError(@NonNull DnsResolver.DnsException error) {
+ mErrorMsg = mMsg + error.getMessage();
+ }
+ }
+
+ public void testQueryForInetAddress() throws Exception {
+ doTestQueryForInetAddress(mExecutor);
+ }
+
+ public void testQueryForInetAddressInline() throws Exception {
+ doTestQueryForInetAddress(mExecutorInline);
+ }
+
+ public void testQueryForInetAddressIpv4() throws Exception {
+ doTestQueryForInetAddressIpv4(mExecutor);
+ }
+
+ public void testQueryForInetAddressIpv4Inline() throws Exception {
+ doTestQueryForInetAddressIpv4(mExecutorInline);
+ }
+
+ public void testQueryForInetAddressIpv6() throws Exception {
+ doTestQueryForInetAddressIpv6(mExecutor);
+ }
+
+ public void testQueryForInetAddressIpv6Inline() throws Exception {
+ doTestQueryForInetAddressIpv6(mExecutorInline);
+ }
+
+ public void testContinuousQueries() throws Exception {
+ doTestContinuousQueries(mExecutor);
+ }
+
+ @SkipPresubmit(reason = "Flaky: b/159762682; add to presubmit after fixing")
+ public void testContinuousQueriesInline() throws Exception {
+ doTestContinuousQueries(mExecutorInline);
+ }
+
+ public void doTestQueryForInetAddress(Executor executor) throws InterruptedException {
+ final String msg = "Test query for InetAddress " + TEST_DOMAIN;
+ for (Network network : getTestableNetworks()) {
+ final VerifyCancelInetAddressCallback callback =
+ new VerifyCancelInetAddressCallback(msg, null);
+ mDns.query(network, TEST_DOMAIN, FLAG_NO_CACHE_LOOKUP, executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertNoError();
+ assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty());
+ }
+ }
+
+ public void testQueryCancelForInetAddress() throws InterruptedException {
+ final String msg = "Test cancel query for InetAddress " + TEST_DOMAIN;
+ // Start a DNS query and the cancel it immediately. Use VerifyCancelInetAddressCallback to
+ // expect that the query is cancelled before it succeeds. If it is not cancelled before it
+ // succeeds, retry the test until it is.
+ for (Network network : getTestableNetworks()) {
+ boolean retry = false;
+ int round = 0;
+ do {
+ if (++round > CANCEL_RETRY_TIMES) {
+ fail(msg + " cancel failed " + CANCEL_RETRY_TIMES + " times");
+ }
+ final CountDownLatch latch = new CountDownLatch(1);
+ final CancellationSignal cancelSignal = new CancellationSignal();
+ final VerifyCancelInetAddressCallback callback =
+ new VerifyCancelInetAddressCallback(msg, cancelSignal);
+ mDns.query(network, TEST_DOMAIN, FLAG_EMPTY, mExecutor, cancelSignal, callback);
+ mExecutor.execute(() -> {
+ cancelSignal.cancel();
+ latch.countDown();
+ });
+
+ retry = callback.needRetry();
+ assertTrue(msg + " query was not cancelled",
+ latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } while (retry);
+ }
+ }
+
+ public void doTestQueryForInetAddressIpv4(Executor executor) throws InterruptedException {
+ final String msg = "Test query for IPv4 InetAddress " + TEST_DOMAIN;
+ for (Network network : getTestableNetworks()) {
+ final VerifyCancelInetAddressCallback callback =
+ new VerifyCancelInetAddressCallback(msg, null);
+ mDns.query(network, TEST_DOMAIN, TYPE_A, FLAG_NO_CACHE_LOOKUP,
+ executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertNoError();
+ assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty());
+ assertTrue(msg + " returned Ipv6 results", !callback.hasIpv6Answer());
+ }
+ }
+
+ public void doTestQueryForInetAddressIpv6(Executor executor) throws InterruptedException {
+ final String msg = "Test query for IPv6 InetAddress " + TEST_DOMAIN;
+ for (Network network : getTestableNetworks()) {
+ final VerifyCancelInetAddressCallback callback =
+ new VerifyCancelInetAddressCallback(msg, null);
+ mDns.query(network, TEST_DOMAIN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
+ executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertNoError();
+ assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty());
+ assertTrue(msg + " returned Ipv4 results", !callback.hasIpv4Answer());
+ }
+ }
+
+ public void testPrivateDnsBypass() throws InterruptedException {
+ final Network[] testNetworks = getTestableNetworks();
+
+ // Set an invalid private DNS server
+ mCtsNetUtils.setPrivateDnsStrictMode(INVALID_PRIVATE_DNS_SERVER);
+ final String msg = "Test PrivateDnsBypass " + TEST_DOMAIN;
+ for (Network network : testNetworks) {
+ // This test cannot be ran with null network because we need to explicitly pass a
+ // private DNS bypassable network or bind one.
+ if (network == null) continue;
+
+ // wait for private DNS setting propagating
+ mCtsNetUtils.awaitPrivateDnsSetting(msg + " wait private DNS setting timeout",
+ network, INVALID_PRIVATE_DNS_SERVER, false);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final DnsResolver.Callback<List<InetAddress>> errorCallback =
+ new DnsResolver.Callback<List<InetAddress>>() {
+ @Override
+ public void onAnswer(@NonNull List<InetAddress> answerList, int rcode) {
+ fail(msg + " should not get valid answer");
+ }
+
+ @Override
+ public void onError(@NonNull DnsResolver.DnsException error) {
+ assertEquals(DnsResolver.ERROR_SYSTEM, error.code);
+ assertEquals(ETIMEDOUT, ((ErrnoException) error.getCause()).errno);
+ latch.countDown();
+ }
+ };
+ // Private DNS strict mode with invalid DNS server is set
+ // Expect no valid answer returned but ErrnoException with ETIMEDOUT
+ mDns.query(network, TEST_DOMAIN, FLAG_NO_CACHE_LOOKUP, mExecutor, null, errorCallback);
+
+ assertTrue(msg + " invalid server round. No response after " + TIMEOUT_MS + "ms.",
+ latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ final VerifyCancelInetAddressCallback callback =
+ new VerifyCancelInetAddressCallback(msg, null);
+ // Bypass privateDns, expect query works fine
+ mDns.query(network.getPrivateDnsBypassingCopy(),
+ TEST_DOMAIN, FLAG_NO_CACHE_LOOKUP, mExecutor, null, callback);
+
+ assertTrue(msg + " bypass private DNS round. No answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertNoError();
+ assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty());
+
+ // To ensure private DNS bypass still work even if passing null network.
+ // Bind process network with a private DNS bypassable network.
+ mCM.bindProcessToNetwork(network.getPrivateDnsBypassingCopy());
+ final VerifyCancelInetAddressCallback callbackWithNullNetwork =
+ new VerifyCancelInetAddressCallback(msg + " with null network ", null);
+ mDns.query(null,
+ TEST_DOMAIN, FLAG_NO_CACHE_LOOKUP, mExecutor, null, callbackWithNullNetwork);
+
+ assertTrue(msg + " with null network bypass private DNS round. No answer after " +
+ TIMEOUT_MS + "ms.", callbackWithNullNetwork.waitForAnswer());
+ callbackWithNullNetwork.assertNoError();
+ assertTrue(msg + " with null network returned 0 results",
+ !callbackWithNullNetwork.isAnswerEmpty());
+
+ // Reset process network to default.
+ mCM.bindProcessToNetwork(null);
+ }
+ }
+
+ public void doTestContinuousQueries(Executor executor) throws InterruptedException {
+ final String msg = "Test continuous " + QUERY_TIMES + " queries " + TEST_DOMAIN;
+ for (Network network : getTestableNetworks()) {
+ for (int i = 0; i < QUERY_TIMES ; ++i) {
+ final VerifyCancelInetAddressCallback callback =
+ new VerifyCancelInetAddressCallback(msg, null);
+ // query v6/v4 in turn
+ boolean queryV6 = (i % 2 == 0);
+ mDns.query(network, TEST_DOMAIN, queryV6 ? TYPE_AAAA : TYPE_A,
+ FLAG_NO_CACHE_LOOKUP, executor, null, callback);
+
+ assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.",
+ callback.waitForAnswer());
+ callback.assertNoError();
+ assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty());
+ assertTrue(msg + " returned " + (queryV6 ? "Ipv4" : "Ipv6") + " results",
+ queryV6 ? !callback.hasIpv4Answer() : !callback.hasIpv6Answer());
+ }
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/DnsTest.java b/tests/cts/net/src/android/net/cts/DnsTest.java
new file mode 100644
index 0000000..fde27e9
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DnsTest.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2013 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.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.os.SystemClock;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import com.android.testutils.SkipPresubmit;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class DnsTest extends AndroidTestCase {
+
+ static {
+ System.loadLibrary("nativedns_jni");
+ }
+
+ private static final boolean DBG = false;
+ private static final String TAG = "DnsTest";
+ private static final String PROXY_NETWORK_TYPE = "PROXY";
+
+ private ConnectivityManager mCm;
+
+ public void setUp() {
+ mCm = getContext().getSystemService(ConnectivityManager.class);
+ }
+
+ /**
+ * @return true on success
+ */
+ private static native boolean testNativeDns();
+
+ /**
+ * Verify:
+ * DNS works - forwards and backwards, giving ipv4 and ipv6
+ * Test that DNS work on v4 and v6 networks
+ * Test Native dns calls (4)
+ * Todo:
+ * Cache is flushed when we change networks
+ * have per-network caches
+ * No cache when there's no network
+ * Perf - measure size of first and second tier caches and their effect
+ * Assert requires network permission
+ */
+ @SkipPresubmit(reason = "IPv6 support may be missing on presubmit virtual hardware")
+ public void testDnsWorks() throws Exception {
+ ensureIpv6Connectivity();
+
+ InetAddress addrs[] = {};
+ try {
+ addrs = InetAddress.getAllByName("www.google.com");
+ } catch (UnknownHostException e) {}
+ assertTrue("[RERUN] DNS could not resolve www.google.com. Check internet connection",
+ addrs.length != 0);
+ boolean foundV4 = false, foundV6 = false;
+ for (InetAddress addr : addrs) {
+ if (addr instanceof Inet4Address) foundV4 = true;
+ else if (addr instanceof Inet6Address) foundV6 = true;
+ if (DBG) Log.e(TAG, "www.google.com gave " + addr.toString());
+ }
+
+ // We should have at least one of the addresses to connect!
+ assertTrue("www.google.com must have IPv4 and/or IPv6 address", foundV4 || foundV6);
+
+ // Skip the rest of the test if the active network for watch is PROXY.
+ // TODO: Check NetworkInfo type in addition to type name once ag/601257 is merged.
+ if (getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
+ && activeNetworkInfoIsProxy()) {
+ Log.i(TAG, "Skipping test because the active network type name is PROXY.");
+ return;
+ }
+
+ // Clear test state so we don't get confused with the previous results.
+ addrs = new InetAddress[0];
+ foundV4 = foundV6 = false;
+ try {
+ addrs = InetAddress.getAllByName("ipv6.google.com");
+ } catch (UnknownHostException e) {}
+ String msg =
+ "[RERUN] DNS could not resolve ipv6.google.com, check the network supports IPv6. lp=" +
+ mCm.getActiveLinkProperties();
+ assertTrue(msg, addrs.length != 0);
+ for (InetAddress addr : addrs) {
+ msg = "[RERUN] ipv6.google.com returned IPv4 address: " + addr.getHostAddress() +
+ ", check your network's DNS server. lp=" + mCm.getActiveLinkProperties();
+ assertFalse (msg, addr instanceof Inet4Address);
+ foundV6 |= (addr instanceof Inet6Address);
+ if (DBG) Log.e(TAG, "ipv6.google.com gave " + addr.toString());
+ }
+
+ assertTrue(foundV6);
+
+ assertTrue(testNativeDns());
+ }
+
+ private static final String[] URLS = { "www.google.com", "ipv6.google.com", "www.yahoo.com",
+ "facebook.com", "youtube.com", "blogspot.com", "baidu.com", "wikipedia.org",
+// live.com fails rev lookup.
+ "twitter.com", "qq.com", "msn.com", "yahoo.co.jp", "linkedin.com",
+ "taobao.com", "google.co.in", "sina.com.cn", "amazon.com", "wordpress.com",
+ "google.co.uk", "ebay.com", "yandex.ru", "163.com", "google.co.jp", "google.fr",
+ "microsoft.com", "paypal.com", "google.com.br", "flickr.com",
+ "mail.ru", "craigslist.org", "fc2.com", "google.it",
+// "apple.com", fails rev lookup
+ "google.es",
+ "imdb.com", "google.ru", "soho.com", "bbc.co.uk", "vkontakte.ru", "ask.com",
+ "tumblr.com", "weibo.com", "go.com", "xvideos.com", "livejasmin.com", "cnn.com",
+ "youku.com", "blogspot.com", "soso.com", "google.ca", "aol.com", "tudou.com",
+ "xhamster.com", "megaupload.com", "ifeng.com", "zedo.com", "mediafire.com", "ameblo.jp",
+ "pornhub.com", "google.co.id", "godaddy.com", "adobe.com", "rakuten.co.jp", "about.com",
+ "espn.go.com", "4shared.com", "alibaba.com","ebay.de", "yieldmanager.com",
+ "wordpress.org", "livejournal.com", "google.com.tr", "google.com.mx", "renren.com",
+ "livedoor.com", "google.com.au", "youporn.com", "uol.com.br", "cnet.com", "conduit.com",
+ "google.pl", "myspace.com", "nytimes.com", "ebay.co.uk", "chinaz.com", "hao123.com",
+ "thepiratebay.org", "doubleclick.com", "alipay.com", "netflix.com", "cnzz.com",
+ "huffingtonpost.com", "twitpic.com", "weather.com", "babylon.com", "amazon.de",
+ "dailymotion.com", "orkut.com", "orkut.com.br", "google.com.sa", "odnoklassniki.ru",
+ "amazon.co.jp", "google.nl", "goo.ne.jp", "stumbleupon.com", "tube8.com", "tmall.com",
+ "imgur.com", "globo.com", "secureserver.net", "fileserve.com", "tianya.cn", "badoo.com",
+ "ehow.com", "photobucket.com", "imageshack.us", "xnxx.com", "deviantart.com",
+ "filestube.com", "addthis.com", "douban.com", "vimeo.com", "sogou.com",
+ "stackoverflow.com", "reddit.com", "dailymail.co.uk", "redtube.com", "megavideo.com",
+ "taringa.net", "pengyou.com", "amazon.co.uk", "fbcdn.net", "aweber.com", "spiegel.de",
+ "rapidshare.com", "mixi.jp", "360buy.com", "google.cn", "digg.com", "answers.com",
+ "bit.ly", "indiatimes.com", "skype.com", "yfrog.com", "optmd.com", "google.com.eg",
+ "google.com.pk", "58.com", "hotfile.com", "google.co.th",
+ "bankofamerica.com", "sourceforge.net", "maktoob.com", "warriorforum.com", "rediff.com",
+ "google.co.za", "56.com", "torrentz.eu", "clicksor.com", "avg.com",
+ "download.com", "ku6.com", "statcounter.com", "foxnews.com", "google.com.ar",
+ "nicovideo.jp", "reference.com", "liveinternet.ru", "ucoz.ru", "xinhuanet.com",
+ "xtendmedia.com", "naver.com", "youjizz.com", "domaintools.com", "sparkstudios.com",
+ "rambler.ru", "scribd.com", "kaixin001.com", "mashable.com", "adultfirendfinder.com",
+ "files.wordpress.com", "guardian.co.uk", "bild.de", "yelp.com", "wikimedia.org",
+ "chase.com", "onet.pl", "ameba.jp", "pconline.com.cn", "free.fr", "etsy.com",
+ "typepad.com", "youdao.com", "megaclick.com", "digitalpoint.com", "blogfa.com",
+ "salesforce.com", "adf.ly", "ganji.com", "wikia.com", "archive.org", "terra.com.br",
+ "w3schools.com", "ezinearticles.com", "wjs.com", "google.com.my", "clickbank.com",
+ "squidoo.com", "hulu.com", "repubblica.it", "google.be", "allegro.pl", "comcast.net",
+ "narod.ru", "zol.com.cn", "orange.fr", "soufun.com", "hatena.ne.jp", "google.gr",
+ "in.com", "techcrunch.com", "orkut.co.in", "xunlei.com",
+ "reuters.com", "google.com.vn", "hostgator.com", "kaskus.us", "espncricinfo.com",
+ "hootsuite.com", "qiyi.com", "gmx.net", "xing.com", "php.net", "soku.com", "web.de",
+ "libero.it", "groupon.com", "51.la", "slideshare.net", "booking.com", "seesaa.net",
+ "126.com", "telegraph.co.uk", "wretch.cc", "twimg.com", "rutracker.org", "angege.com",
+ "nba.com", "dell.com", "leboncoin.fr", "people.com", "google.com.tw", "walmart.com",
+ "daum.net", "2ch.net", "constantcontact.com", "nifty.com", "mywebsearch.com",
+ "tripadvisor.com", "google.se", "paipai.com", "google.com.ua", "ning.com", "hp.com",
+ "google.at", "joomla.org", "icio.us", "hudong.com", "csdn.net", "getfirebug.com",
+ "ups.com", "cj.com", "google.ch", "camzap.com", "wordreference.com", "tagged.com",
+ "wp.pl", "mozilla.com", "google.ru", "usps.com", "china.com", "themeforest.net",
+ "search-results.com", "tribalfusion.com", "thefreedictionary.com", "isohunt.com",
+ "linkwithin.com", "cam4.com", "plentyoffish.com", "wellsfargo.com", "metacafe.com",
+ "depositfiles.com", "freelancer.com", "opendns.com", "homeway.com", "engadget.com",
+ "10086.cn", "360.cn", "marca.com", "dropbox.com", "ign.com", "match.com", "google.pt",
+ "facemoods.com", "hardsextube.com", "google.com.ph", "lockerz.com", "istockphoto.com",
+ "partypoker.com", "netlog.com", "outbrain.com", "elpais.com", "fiverr.com",
+ "biglobe.ne.jp", "corriere.it", "love21cn.com", "yesky.com", "spankwire.com",
+ "ig.com.br", "imagevenue.com", "hubpages.com", "google.co.ve"};
+
+// TODO - this works, but is slow and cts doesn't do anything with the result.
+// Maybe require a min performance, a min cache size (detectable) and/or move
+// to perf testing
+ private static final int LOOKUP_COUNT_GOAL = URLS.length;
+ public void skiptestDnsPerf() {
+ ArrayList<String> results = new ArrayList<String>();
+ int failures = 0;
+ try {
+ for (int numberOfUrls = URLS.length; numberOfUrls > 0; numberOfUrls--) {
+ failures = 0;
+ int iterationLimit = LOOKUP_COUNT_GOAL / numberOfUrls;
+ long startTime = SystemClock.elapsedRealtimeNanos();
+ for (int iteration = 0; iteration < iterationLimit; iteration++) {
+ for (int urlIndex = 0; urlIndex < numberOfUrls; urlIndex++) {
+ try {
+ InetAddress addr = InetAddress.getByName(URLS[urlIndex]);
+ } catch (UnknownHostException e) {
+ Log.e(TAG, "failed first lookup of " + URLS[urlIndex]);
+ failures++;
+ try {
+ InetAddress addr = InetAddress.getByName(URLS[urlIndex]);
+ } catch (UnknownHostException ee) {
+ failures++;
+ Log.e(TAG, "failed SECOND lookup of " + URLS[urlIndex]);
+ }
+ }
+ }
+ }
+ long endTime = SystemClock.elapsedRealtimeNanos();
+ float nsPer = ((float)(endTime-startTime) / iterationLimit) / numberOfUrls/ 1000;
+ String thisResult = new String("getByName for " + numberOfUrls + " took " +
+ (endTime - startTime)/1000 + "(" + nsPer + ") with " +
+ failures + " failures\n");
+ Log.d(TAG, thisResult);
+ results.add(thisResult);
+ }
+ // build up a list of addresses
+ ArrayList<byte[]> addressList = new ArrayList<byte[]>();
+ for (String url : URLS) {
+ try {
+ InetAddress addr = InetAddress.getByName(url);
+ addressList.add(addr.getAddress());
+ } catch (UnknownHostException e) {
+ Log.e(TAG, "Exception making reverseDNS list: " + e.toString());
+ }
+ }
+ for (int numberOfAddrs = addressList.size(); numberOfAddrs > 0; numberOfAddrs--) {
+ int iterationLimit = LOOKUP_COUNT_GOAL / numberOfAddrs;
+ failures = 0;
+ long startTime = SystemClock.elapsedRealtimeNanos();
+ for (int iteration = 0; iteration < iterationLimit; iteration++) {
+ for (int addrIndex = 0; addrIndex < numberOfAddrs; addrIndex++) {
+ try {
+ InetAddress addr = InetAddress.getByAddress(addressList.get(addrIndex));
+ String hostname = addr.getHostName();
+ } catch (UnknownHostException e) {
+ failures++;
+ Log.e(TAG, "Failure doing reverse DNS lookup: " + e.toString());
+ try {
+ InetAddress addr =
+ InetAddress.getByAddress(addressList.get(addrIndex));
+ String hostname = addr.getHostName();
+
+ } catch (UnknownHostException ee) {
+ failures++;
+ Log.e(TAG, "Failure doing SECOND reverse DNS lookup: " +
+ ee.toString());
+ }
+ }
+ }
+ }
+ long endTime = SystemClock.elapsedRealtimeNanos();
+ float nsPer = ((endTime-startTime) / iterationLimit) / numberOfAddrs / 1000;
+ String thisResult = new String("getHostName for " + numberOfAddrs + " took " +
+ (endTime - startTime)/1000 + "(" + nsPer + ") with " +
+ failures + " failures\n");
+ Log.d(TAG, thisResult);
+ results.add(thisResult);
+ }
+ for (String result : results) Log.d(TAG, result);
+
+ InetAddress exit = InetAddress.getByName("exitrightnow.com");
+ Log.e(TAG, " exit address= "+exit.toString());
+
+ } catch (Exception e) {
+ Log.e(TAG, "bad URL in testDnsPerf: " + e.toString());
+ }
+ }
+
+ private boolean activeNetworkInfoIsProxy() {
+ NetworkInfo info = mCm.getActiveNetworkInfo();
+ if (PROXY_NETWORK_TYPE.equals(info.getTypeName())) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private void ensureIpv6Connectivity() throws InterruptedException {
+ CountDownLatch latch = new CountDownLatch(1);
+ final int TIMEOUT_MS = 5_000;
+
+ final NetworkCallback callback = new NetworkCallback() {
+ @Override
+ public void onLinkPropertiesChanged(Network network, LinkProperties lp) {
+ if (lp.hasGlobalIpv6Address()) {
+ latch.countDown();
+ }
+ }
+ };
+ mCm.registerDefaultNetworkCallback(callback);
+
+ String msg = "Default network did not provide IPv6 connectivity after " + TIMEOUT_MS
+ + "ms. Please connect to an IPv6-capable network. lp="
+ + mCm.getActiveLinkProperties();
+ try {
+ assertTrue(msg, latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/IkeTunUtils.java b/tests/cts/net/src/android/net/cts/IkeTunUtils.java
new file mode 100644
index 0000000..fc25292
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IkeTunUtils.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2020 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 static android.net.cts.PacketUtils.BytePayload;
+import static android.net.cts.PacketUtils.IP4_HDRLEN;
+import static android.net.cts.PacketUtils.IP6_HDRLEN;
+import static android.net.cts.PacketUtils.IpHeader;
+import static android.net.cts.PacketUtils.UDP_HDRLEN;
+import static android.net.cts.PacketUtils.UdpHeader;
+import static android.net.cts.PacketUtils.getIpHeader;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import android.os.ParcelFileDescriptor;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+// TODO: Merge this with the version in the IPsec module (IKEv2 library) CTS tests.
+/** An extension of the TunUtils class with IKE-specific packet handling. */
+public class IkeTunUtils extends TunUtils {
+ private static final int PORT_LEN = 2;
+
+ private static final byte[] NON_ESP_MARKER = new byte[] {0, 0, 0, 0};
+
+ private static final int IKE_HEADER_LEN = 28;
+ private static final int IKE_SPI_LEN = 8;
+ private static final int IKE_IS_RESP_BYTE_OFFSET = 19;
+ private static final int IKE_MSG_ID_OFFSET = 20;
+ private static final int IKE_MSG_ID_LEN = 4;
+
+ public IkeTunUtils(ParcelFileDescriptor tunFd) {
+ super(tunFd);
+ }
+
+ /**
+ * Await an expected IKE request and inject an IKE response.
+ *
+ * @param respIkePkt IKE response packet without IP/UDP headers or NON ESP MARKER.
+ */
+ public byte[] awaitReqAndInjectResp(long expectedInitIkeSpi, int expectedMsgId,
+ boolean encapExpected, byte[] respIkePkt) throws Exception {
+ final byte[] request = awaitIkePacket(expectedInitIkeSpi, expectedMsgId, encapExpected);
+
+ // Build response header by flipping address and port
+ final InetAddress srcAddr = getDstAddress(request);
+ final InetAddress dstAddr = getSrcAddress(request);
+ final int srcPort = getDstPort(request);
+ final int dstPort = getSrcPort(request);
+
+ final byte[] response =
+ buildIkePacket(srcAddr, dstAddr, srcPort, dstPort, encapExpected, respIkePkt);
+ injectPacket(response);
+ return request;
+ }
+
+ private byte[] awaitIkePacket(long expectedInitIkeSpi, int expectedMsgId, boolean expectEncap)
+ throws Exception {
+ return super.awaitPacket(pkt -> isIke(pkt, expectedInitIkeSpi, expectedMsgId, expectEncap));
+ }
+
+ private static boolean isIke(
+ byte[] pkt, long expectedInitIkeSpi, int expectedMsgId, boolean encapExpected) {
+ final int ipProtocolOffset;
+ final int ikeOffset;
+
+ if (isIpv6(pkt)) {
+ ipProtocolOffset = IP6_PROTO_OFFSET;
+ ikeOffset = IP6_HDRLEN + UDP_HDRLEN;
+ } else {
+ if (encapExpected && !hasNonEspMarkerv4(pkt)) {
+ return false;
+ }
+
+ // Use default IPv4 header length (assuming no options)
+ final int encapMarkerLen = encapExpected ? NON_ESP_MARKER.length : 0;
+ ipProtocolOffset = IP4_PROTO_OFFSET;
+ ikeOffset = IP4_HDRLEN + UDP_HDRLEN + encapMarkerLen;
+ }
+
+ return pkt[ipProtocolOffset] == IPPROTO_UDP
+ && areSpiAndMsgIdEqual(pkt, ikeOffset, expectedInitIkeSpi, expectedMsgId);
+ }
+
+ /** Checks if the provided IPv4 packet has a UDP-encapsulation NON-ESP marker */
+ private static boolean hasNonEspMarkerv4(byte[] ipv4Pkt) {
+ final int nonEspMarkerOffset = IP4_HDRLEN + UDP_HDRLEN;
+ if (ipv4Pkt.length < nonEspMarkerOffset + NON_ESP_MARKER.length) {
+ return false;
+ }
+
+ final byte[] nonEspMarker = Arrays.copyOfRange(
+ ipv4Pkt, nonEspMarkerOffset, nonEspMarkerOffset + NON_ESP_MARKER.length);
+ return Arrays.equals(NON_ESP_MARKER, nonEspMarker);
+ }
+
+ private static boolean areSpiAndMsgIdEqual(
+ byte[] pkt, int ikeOffset, long expectedIkeInitSpi, int expectedMsgId) {
+ if (pkt.length <= ikeOffset + IKE_HEADER_LEN) {
+ return false;
+ }
+
+ final ByteBuffer buffer = ByteBuffer.wrap(pkt);
+ final long spi = buffer.getLong(ikeOffset);
+ final int msgId = buffer.getInt(ikeOffset + IKE_MSG_ID_OFFSET);
+
+ return expectedIkeInitSpi == spi && expectedMsgId == msgId;
+ }
+
+ private static InetAddress getSrcAddress(byte[] pkt) throws Exception {
+ return getAddress(pkt, true);
+ }
+
+ private static InetAddress getDstAddress(byte[] pkt) throws Exception {
+ return getAddress(pkt, false);
+ }
+
+ private static InetAddress getAddress(byte[] pkt, boolean getSrcAddr) throws Exception {
+ final int ipLen = isIpv6(pkt) ? IP6_ADDR_LEN : IP4_ADDR_LEN;
+ final int srcIpOffset = isIpv6(pkt) ? IP6_ADDR_OFFSET : IP4_ADDR_OFFSET;
+ final int ipOffset = getSrcAddr ? srcIpOffset : srcIpOffset + ipLen;
+
+ if (pkt.length < ipOffset + ipLen) {
+ // Should be impossible; getAddress() is only called with a full IKE request including
+ // the IP and UDP headers.
+ throw new IllegalArgumentException("Packet was too short to contain IP address");
+ }
+
+ return InetAddress.getByAddress(Arrays.copyOfRange(pkt, ipOffset, ipOffset + ipLen));
+ }
+
+ private static int getSrcPort(byte[] pkt) throws Exception {
+ return getPort(pkt, true);
+ }
+
+ private static int getDstPort(byte[] pkt) throws Exception {
+ return getPort(pkt, false);
+ }
+
+ private static int getPort(byte[] pkt, boolean getSrcPort) {
+ final int srcPortOffset = isIpv6(pkt) ? IP6_HDRLEN : IP4_HDRLEN;
+ final int portOffset = getSrcPort ? srcPortOffset : srcPortOffset + PORT_LEN;
+
+ if (pkt.length < portOffset + PORT_LEN) {
+ // Should be impossible; getPort() is only called with a full IKE request including the
+ // IP and UDP headers.
+ throw new IllegalArgumentException("Packet was too short to contain port");
+ }
+
+ final ByteBuffer buffer = ByteBuffer.wrap(pkt);
+ return Short.toUnsignedInt(buffer.getShort(portOffset));
+ }
+
+ private static byte[] buildIkePacket(
+ InetAddress srcAddr,
+ InetAddress dstAddr,
+ int srcPort,
+ int dstPort,
+ boolean useEncap,
+ byte[] payload)
+ throws Exception {
+ // Append non-ESP marker if encap is enabled
+ if (useEncap) {
+ final ByteBuffer buffer = ByteBuffer.allocate(NON_ESP_MARKER.length + payload.length);
+ buffer.put(NON_ESP_MARKER);
+ buffer.put(payload);
+ payload = buffer.array();
+ }
+
+ final UdpHeader udpPkt = new UdpHeader(srcPort, dstPort, new BytePayload(payload));
+ final IpHeader ipPkt = getIpHeader(udpPkt.getProtocolId(), srcAddr, dstAddr, udpPkt);
+ return ipPkt.getPacketBytes();
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
new file mode 100644
index 0000000..9eab024
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -0,0 +1,535 @@
+/*
+ * Copyright (C) 2020 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 static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.Ikev2VpnProfile;
+import android.net.IpSecAlgorithm;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.ProxyInfo;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.VpnManager;
+import android.net.cts.util.CtsNetUtils;
+import android.os.Build;
+import android.os.Process;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.util.HexDump;
+import com.android.org.bouncycastle.x509.X509V1CertificateGenerator;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.security.auth.x500.X500Principal;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@IgnoreUpTo(Build.VERSION_CODES.Q)
+@AppModeFull(reason = "Appops state changes disallowed for instant apps (OP_ACTIVATE_PLATFORM_VPN)")
+public class Ikev2VpnTest {
+ private static final String TAG = Ikev2VpnTest.class.getSimpleName();
+
+ // Test vectors for IKE negotiation in test mode.
+ private static final String SUCCESSFUL_IKE_INIT_RESP_V4 =
+ "46b8eca1e0d72a18b2b5d9006d47a0022120222000000000000002d0220000300000002c01010004030000"
+ + "0c0100000c800e0100030000080300000c030000080200000400000008040000102800020800"
+ + "100000b8070f159fe5141d8754ca86f72ecc28d66f514927e96cbe9eec0adb42bf2c276a0ab7"
+ + "a97fa93555f4be9218c14e7f286bb28c6b4fb13825a420f2ffc165854f200bab37d69c8963d4"
+ + "0acb831d983163aa50622fd35c182efe882cf54d6106222abcfaa597255d302f1b95ab71c142"
+ + "c279ea5839a180070bff73f9d03fab815f0d5ee2adec7e409d1e35979f8bd92ffd8aab13d1a0"
+ + "0657d816643ae767e9ae84d2ccfa2bcce1a50572be8d3748ae4863c41ae90da16271e014270f"
+ + "77edd5cd2e3299f3ab27d7203f93d770bacf816041cdcecd0f9af249033979da4369cb242dd9"
+ + "6d172e60513ff3db02de63e50eb7d7f596ada55d7946cad0af0669d1f3e2804846ab3f2a930d"
+ + "df56f7f025f25c25ada694e6231abbb87ee8cfd072c8481dc0b0f6b083fdc3bd89b080e49feb"
+ + "0288eef6fdf8a26ee2fc564a11e7385215cf2deaf2a9965638fc279c908ccdf04094988d91a2"
+ + "464b4a8c0326533aff5119ed79ecbd9d99a218b44f506a5eb09351e67da86698b4c58718db25"
+ + "d55f426fb4c76471b27a41fbce00777bc233c7f6e842e39146f466826de94f564cad8b92bfbe"
+ + "87c99c4c7973ec5f1eea8795e7da82819753aa7c4fcfdab77066c56b939330c4b0d354c23f83"
+ + "ea82fa7a64c4b108f1188379ea0eb4918ee009d804100e6bf118771b9058d42141c847d5ec37"
+ + "6e5ec591c71fc9dac01063c2bd31f9c783b28bf1182900002430f3d5de3449462b31dd28bc27"
+ + "297b6ad169bccce4f66c5399c6e0be9120166f2900001c0000400428b8df2e66f69c8584a186"
+ + "c5eac66783551d49b72900001c000040054e7a622e802d5cbfb96d5f30a6e433994370173529"
+ + "0000080000402e290000100000402f00020003000400050000000800004014";
+ private static final String SUCCESSFUL_IKE_INIT_RESP_V6 =
+ "46b8eca1e0d72a1800d9ea1babce26bf2120222000000000000002d0220000300000002c01010004030000"
+ + "0c0100000c800e0100030000080300000c030000080200000400000008040000102800020800"
+ + "100000ea0e6dd9ca5930a9a45c323a41f64bfd8cdef7730f5fbff37d7c377da427f489a42aa8"
+ + "c89233380e6e925990d49de35c2cdcf63a61302c731a4b3569df1ee1bf2457e55a6751838ede"
+ + "abb75cc63ba5c9e4355e8e784f383a5efe8a44727dc14aeaf8dacc2620fb1c8875416dc07739"
+ + "7fe4decc1bd514a9c7d270cf21fd734c63a25c34b30b68686e54e8a198f37f27cb491fe27235"
+ + "fab5476b036d875ccab9a68d65fbf3006197f9bebbf94de0d3802b4fafe1d48d931ce3a1a346"
+ + "2d65bd639e9bd7fa46299650a9dbaf9b324e40b466942d91a59f41ef8042f8474c4850ed0f63"
+ + "e9238949d41cd8bbaea9aefdb65443a6405792839563aa5dc5c36b5ce8326ccf8a94d9622b85"
+ + "038d390d5fc0299e14e1f022966d4ac66515f6108ca04faec44821fe5bbf2ed4f84ff5671219"
+ + "608cb4c36b44a31ba010c9088f8d5ff943bb9ff857f74be1755f57a5783874adc57f42bb174e"
+ + "4ad3215de628707014dbcb1707bd214658118fdd7a42b3e1638b991ce5b812a667f1145be811"
+ + "685e3cd3baf9b18d062657b64c206a4d19a531c252a6a51a04aeaf42c618620cdbab65baca23"
+ + "82c57ed888422aeaacf7f1bc3fe2247ff7e7eaca218b74d7b31d02f2b0afa123f802529e7e6c"
+ + "3259d418290740ddbf55686e26998d7edcbbf895664972fed666f2f20af40503aa2af436ec6d"
+ + "4ec981ab19b9088755d94ae7a7c2066ea331d4e56e290000243fefe5555fce552d57a84e682c"
+ + "d4a6dfb3f2f94a94464d5bec3d88b88e9559642900001c00004004eb4afff764e7b79bca78b1"
+ + "3a89100d36d678ae982900001c00004005d177216a3c26f782076e12570d40bfaaa148822929"
+ + "0000080000402e290000100000402f00020003000400050000000800004014";
+ private static final String SUCCESSFUL_IKE_AUTH_RESP_V4 =
+ "46b8eca1e0d72a18b2b5d9006d47a0022e20232000000001000000e0240000c420a2500a3da4c66fa6929e"
+ + "600f36349ba0e38de14f78a3ad0416cba8c058735712a3d3f9a0a6ed36de09b5e9e02697e7c4"
+ + "2d210ac86cfbd709503cfa51e2eab8cfdc6427d136313c072968f6506a546eb5927164200592"
+ + "6e36a16ee994e63f029432a67bc7d37ca619e1bd6e1678df14853067ecf816b48b81e8746069"
+ + "406363e5aa55f13cb2afda9dbebee94256c29d630b17dd7f1ee52351f92b6e1c3d8551c513f1"
+ + "d74ac52a80b2041397e109fe0aeb3c105b0d4be0ae343a943398764281";
+ private static final String SUCCESSFUL_IKE_AUTH_RESP_V6 =
+ "46b8eca1e0d72a1800d9ea1babce26bf2e20232000000001000000f0240000d4aaf6eaa6c06b50447e6f54"
+ + "827fd8a9d9d6ac8015c1ebb3e8cb03fc6e54b49a107441f50004027cc5021600828026367f03"
+ + "bc425821cd7772ee98637361300c9b76056e874fea2bd4a17212370b291894264d8c023a01d1"
+ + "c3b691fd4b7c0b534e8c95af4c4638e2d125cb21c6267e2507cd745d72e8da109c47b9259c6c"
+ + "57a26f6bc5b337b9b9496d54bdde0333d7a32e6e1335c9ee730c3ecd607a8689aa7b0577b74f"
+ + "3bf437696a9fd5fc0aee3ed346cd9e15d1dda293df89eb388a8719388a60ca7625754de12cdb"
+ + "efe4c886c5c401";
+ private static final long IKE_INITIATOR_SPI = Long.parseLong("46B8ECA1E0D72A18", 16);
+
+ private static final InetAddress LOCAL_OUTER_4 = InetAddress.parseNumericAddress("192.0.2.1");
+ private static final InetAddress LOCAL_OUTER_6 =
+ InetAddress.parseNumericAddress("2001:db8::1");
+
+ private static final int IP4_PREFIX_LEN = 32;
+ private static final int IP6_PREFIX_LEN = 128;
+
+ // TODO: Use IPv6 address when we can generate test vectors (GCE does not allow IPv6 yet).
+ private static final String TEST_SERVER_ADDR_V4 = "192.0.2.2";
+ private static final String TEST_SERVER_ADDR_V6 = "2001:db8::2";
+ private static final String TEST_IDENTITY = "client.cts.android.com";
+ private static final List<String> TEST_ALLOWED_ALGORITHMS =
+ Arrays.asList(IpSecAlgorithm.AUTH_CRYPT_AES_GCM);
+
+ private static final ProxyInfo TEST_PROXY_INFO =
+ ProxyInfo.buildDirectProxy("proxy.cts.android.com", 1234);
+ private static final int TEST_MTU = 1300;
+
+ private static final byte[] TEST_PSK = "ikeAndroidPsk".getBytes();
+ private static final String TEST_USER = "username";
+ private static final String TEST_PASSWORD = "pa55w0rd";
+
+ // Static state to reduce setup/teardown
+ private static final Context sContext = InstrumentationRegistry.getContext();
+ private static final ConnectivityManager sCM =
+ (ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ private static final VpnManager sVpnMgr =
+ (VpnManager) sContext.getSystemService(Context.VPN_MANAGEMENT_SERVICE);
+ private static final CtsNetUtils mCtsNetUtils = new CtsNetUtils(sContext);
+
+ private final X509Certificate mServerRootCa;
+ private final CertificateAndKey mUserCertKey;
+
+ public Ikev2VpnTest() throws Exception {
+ // Build certificates
+ mServerRootCa = generateRandomCertAndKeyPair().cert;
+ mUserCertKey = generateRandomCertAndKeyPair();
+ }
+
+ @After
+ public void tearDown() {
+ setAppop(AppOpsManager.OP_ACTIVATE_VPN, false);
+ setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false);
+ }
+
+ /**
+ * Sets the given appop using shell commands
+ *
+ * <p>This method must NEVER be called from within a shell permission, as it will attempt to
+ * acquire, and then drop the shell permission identity. This results in the caller losing the
+ * shell permission identity due to these calls not being reference counted.
+ */
+ public void setAppop(int appop, boolean allow) {
+ // Requires shell permission to update appops.
+ runWithShellPermissionIdentity(() -> {
+ mCtsNetUtils.setAppopPrivileged(appop, allow);
+ }, Manifest.permission.MANAGE_TEST_NETWORKS);
+ }
+
+ private Ikev2VpnProfile buildIkev2VpnProfileCommon(
+ Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks) throws Exception {
+ if (isRestrictedToTestNetworks) {
+ builder.restrictToTestNetworks();
+ }
+
+ return builder.setBypassable(true)
+ .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
+ .setProxy(TEST_PROXY_INFO)
+ .setMaxMtu(TEST_MTU)
+ .setMetered(false)
+ .build();
+ }
+
+ private Ikev2VpnProfile buildIkev2VpnProfilePsk(boolean isRestrictedToTestNetworks)
+ throws Exception {
+ return buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6, isRestrictedToTestNetworks);
+ }
+
+ private Ikev2VpnProfile buildIkev2VpnProfilePsk(
+ String remote, boolean isRestrictedToTestNetworks) throws Exception {
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY).setAuthPsk(TEST_PSK);
+
+ return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks);
+ }
+
+ private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks)
+ throws Exception {
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+ .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
+
+ return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks);
+ }
+
+ private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks)
+ throws Exception {
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+ .setAuthDigitalSignature(
+ mUserCertKey.cert, mUserCertKey.key, mServerRootCa);
+
+ return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks);
+ }
+
+ private void checkBasicIkev2VpnProfile(@NonNull Ikev2VpnProfile profile) throws Exception {
+ assertEquals(TEST_SERVER_ADDR_V6, profile.getServerAddr());
+ assertEquals(TEST_IDENTITY, profile.getUserIdentity());
+ assertEquals(TEST_PROXY_INFO, profile.getProxyInfo());
+ assertEquals(TEST_ALLOWED_ALGORITHMS, profile.getAllowedAlgorithms());
+ assertTrue(profile.isBypassable());
+ assertFalse(profile.isMetered());
+ assertEquals(TEST_MTU, profile.getMaxMtu());
+ assertFalse(profile.isRestrictedToTestNetworks());
+ }
+
+ @Test
+ public void testBuildIkev2VpnProfilePsk() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ final Ikev2VpnProfile profile =
+ buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */);
+
+ checkBasicIkev2VpnProfile(profile);
+ assertArrayEquals(TEST_PSK, profile.getPresharedKey());
+
+ // Verify nothing else is set.
+ assertNull(profile.getUsername());
+ assertNull(profile.getPassword());
+ assertNull(profile.getServerRootCaCert());
+ assertNull(profile.getRsaPrivateKey());
+ assertNull(profile.getUserCert());
+ }
+
+ @Test
+ public void testBuildIkev2VpnProfileUsernamePassword() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ final Ikev2VpnProfile profile =
+ buildIkev2VpnProfileUsernamePassword(false /* isRestrictedToTestNetworks */);
+
+ checkBasicIkev2VpnProfile(profile);
+ assertEquals(TEST_USER, profile.getUsername());
+ assertEquals(TEST_PASSWORD, profile.getPassword());
+ assertEquals(mServerRootCa, profile.getServerRootCaCert());
+
+ // Verify nothing else is set.
+ assertNull(profile.getPresharedKey());
+ assertNull(profile.getRsaPrivateKey());
+ assertNull(profile.getUserCert());
+ }
+
+ @Test
+ public void testBuildIkev2VpnProfileDigitalSignature() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ final Ikev2VpnProfile profile =
+ buildIkev2VpnProfileDigitalSignature(false /* isRestrictedToTestNetworks */);
+
+ checkBasicIkev2VpnProfile(profile);
+ assertEquals(mUserCertKey.cert, profile.getUserCert());
+ assertEquals(mUserCertKey.key, profile.getRsaPrivateKey());
+ assertEquals(mServerRootCa, profile.getServerRootCaCert());
+
+ // Verify nothing else is set.
+ assertNull(profile.getUsername());
+ assertNull(profile.getPassword());
+ assertNull(profile.getPresharedKey());
+ }
+
+ private void verifyProvisionVpnProfile(
+ boolean hasActivateVpn, boolean hasActivatePlatformVpn, boolean expectIntent)
+ throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ setAppop(AppOpsManager.OP_ACTIVATE_VPN, hasActivateVpn);
+ setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, hasActivatePlatformVpn);
+
+ final Ikev2VpnProfile profile =
+ buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */);
+ final Intent intent = sVpnMgr.provisionVpnProfile(profile);
+ assertEquals(expectIntent, intent != null);
+ }
+
+ @Test
+ public void testProvisionVpnProfileNoPreviousConsent() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ verifyProvisionVpnProfile(false /* hasActivateVpn */,
+ false /* hasActivatePlatformVpn */, true /* expectIntent */);
+ }
+
+ @Test
+ public void testProvisionVpnProfilePlatformVpnConsented() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ verifyProvisionVpnProfile(false /* hasActivateVpn */,
+ true /* hasActivatePlatformVpn */, false /* expectIntent */);
+ }
+
+ @Test
+ public void testProvisionVpnProfileVpnServiceConsented() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ verifyProvisionVpnProfile(true /* hasActivateVpn */,
+ false /* hasActivatePlatformVpn */, false /* expectIntent */);
+ }
+
+ @Test
+ public void testProvisionVpnProfileAllPreConsented() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ verifyProvisionVpnProfile(true /* hasActivateVpn */,
+ true /* hasActivatePlatformVpn */, false /* expectIntent */);
+ }
+
+ @Test
+ public void testDeleteVpnProfile() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true);
+
+ final Ikev2VpnProfile profile =
+ buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */);
+ assertNull(sVpnMgr.provisionVpnProfile(profile));
+
+ // Verify that deleting the profile works (even without the appop)
+ setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false);
+ sVpnMgr.deleteProvisionedVpnProfile();
+
+ // Test that the profile was deleted - starting it should throw an IAE.
+ try {
+ setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true);
+ sVpnMgr.startProvisionedVpnProfile();
+ fail("Expected IllegalArgumentException due to missing profile");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
+ public void testStartVpnProfileNoPreviousConsent() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ setAppop(AppOpsManager.OP_ACTIVATE_VPN, false);
+ setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false);
+
+ // Make sure the VpnProfile is not provisioned already.
+ sVpnMgr.stopProvisionedVpnProfile();
+
+ try {
+ sVpnMgr.startProvisionedVpnProfile();
+ fail("Expected SecurityException for missing consent");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ private void checkStartStopVpnProfileBuildsNetworks(IkeTunUtils tunUtils, boolean testIpv6)
+ throws Exception {
+ String serverAddr = testIpv6 ? TEST_SERVER_ADDR_V6 : TEST_SERVER_ADDR_V4;
+ String initResp = testIpv6 ? SUCCESSFUL_IKE_INIT_RESP_V6 : SUCCESSFUL_IKE_INIT_RESP_V4;
+ String authResp = testIpv6 ? SUCCESSFUL_IKE_AUTH_RESP_V6 : SUCCESSFUL_IKE_AUTH_RESP_V4;
+ boolean hasNat = !testIpv6;
+
+ // Requires MANAGE_TEST_NETWORKS to provision a test-mode profile.
+ mCtsNetUtils.setAppopPrivileged(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true);
+
+ final Ikev2VpnProfile profile =
+ buildIkev2VpnProfilePsk(serverAddr, true /* isRestrictedToTestNetworks */);
+ assertNull(sVpnMgr.provisionVpnProfile(profile));
+
+ sVpnMgr.startProvisionedVpnProfile();
+
+ // Inject IKE negotiation
+ int expectedMsgId = 0;
+ tunUtils.awaitReqAndInjectResp(IKE_INITIATOR_SPI, expectedMsgId++, false /* isEncap */,
+ HexDump.hexStringToByteArray(initResp));
+ tunUtils.awaitReqAndInjectResp(IKE_INITIATOR_SPI, expectedMsgId++, hasNat /* isEncap */,
+ HexDump.hexStringToByteArray(authResp));
+
+ // Verify the VPN network came up
+ final NetworkRequest nr = new NetworkRequest.Builder()
+ .clearCapabilities().addTransportType(TRANSPORT_VPN).build();
+
+ final TestNetworkCallback cb = new TestNetworkCallback();
+ sCM.requestNetwork(nr, cb);
+ cb.waitForAvailable();
+ final Network vpnNetwork = cb.currentNetwork;
+ assertNotNull(vpnNetwork);
+
+ final NetworkCapabilities caps = sCM.getNetworkCapabilities(vpnNetwork);
+ assertTrue(caps.hasTransport(TRANSPORT_VPN));
+ assertTrue(caps.hasCapability(NET_CAPABILITY_INTERNET));
+ assertEquals(Process.myUid(), caps.getOwnerUid());
+
+ sVpnMgr.stopProvisionedVpnProfile();
+ cb.waitForLost();
+ assertEquals(vpnNetwork, cb.lastLostNetwork);
+ }
+
+ private void doTestStartStopVpnProfile(boolean testIpv6) throws Exception {
+ // Non-final; these variables ensure we clean up properly after our test if we have
+ // allocated test network resources
+ final TestNetworkManager tnm = sContext.getSystemService(TestNetworkManager.class);
+ TestNetworkInterface testIface = null;
+ TestNetworkCallback tunNetworkCallback = null;
+
+ try {
+ // Build underlying test network
+ testIface = tnm.createTunInterface(
+ new LinkAddress[] {
+ new LinkAddress(LOCAL_OUTER_4, IP4_PREFIX_LEN),
+ new LinkAddress(LOCAL_OUTER_6, IP6_PREFIX_LEN)});
+
+ // Hold on to this callback to ensure network does not get reaped.
+ tunNetworkCallback = mCtsNetUtils.setupAndGetTestNetwork(
+ testIface.getInterfaceName());
+ final IkeTunUtils tunUtils = new IkeTunUtils(testIface.getFileDescriptor());
+
+ checkStartStopVpnProfileBuildsNetworks(tunUtils, testIpv6);
+ } finally {
+ // Make sure to stop the VPN profile. This is safe to call multiple times.
+ sVpnMgr.stopProvisionedVpnProfile();
+
+ if (testIface != null) {
+ testIface.getFileDescriptor().close();
+ }
+
+ if (tunNetworkCallback != null) {
+ sCM.unregisterNetworkCallback(tunNetworkCallback);
+ }
+
+ final Network testNetwork = tunNetworkCallback.currentNetwork;
+ if (testNetwork != null) {
+ tnm.teardownTestNetwork(testNetwork);
+ }
+ }
+ }
+
+ @Test
+ public void testStartStopVpnProfileV4() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ // Requires shell permission to update appops.
+ runWithShellPermissionIdentity(() -> {
+ doTestStartStopVpnProfile(false);
+ });
+ }
+
+ @Test
+ public void testStartStopVpnProfileV6() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ // Requires shell permission to update appops.
+ runWithShellPermissionIdentity(() -> {
+ doTestStartStopVpnProfile(true);
+ });
+ }
+
+ private static class CertificateAndKey {
+ public final X509Certificate cert;
+ public final PrivateKey key;
+
+ CertificateAndKey(X509Certificate cert, PrivateKey key) {
+ this.cert = cert;
+ this.key = key;
+ }
+ }
+
+ private static CertificateAndKey generateRandomCertAndKeyPair() throws Exception {
+ final Date validityBeginDate =
+ new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1L));
+ final Date validityEndDate =
+ new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1L));
+
+ // Generate a keypair
+ final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(512);
+ final KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ final X500Principal dnName = new X500Principal("CN=test.android.com");
+ final X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
+ certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
+ certGen.setSubjectDN(dnName);
+ certGen.setIssuerDN(dnName);
+ certGen.setNotBefore(validityBeginDate);
+ certGen.setNotAfter(validityEndDate);
+ certGen.setPublicKey(keyPair.getPublic());
+ certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
+
+ final X509Certificate cert = certGen.generate(keyPair.getPrivate(), "AndroidOpenSSL");
+ return new CertificateAndKey(cert, keyPair.getPrivate());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/InetAddressesTest.java b/tests/cts/net/src/android/net/cts/InetAddressesTest.java
new file mode 100644
index 0000000..7837ce9
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/InetAddressesTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2018 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.InetAddresses;
+import java.net.InetAddress;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(JUnitParamsRunner.class)
+public class InetAddressesTest {
+
+ public static String[][] validNumericAddressesAndStringRepresentation() {
+ return new String[][] {
+ // Regular IPv4.
+ { "1.2.3.4", "1.2.3.4" },
+
+ // Regular IPv6.
+ { "2001:4860:800d::68", "2001:4860:800d::68" },
+ { "1234:5678::9ABC:DEF0", "1234:5678::9abc:def0" },
+ { "2001:cdba:9abc:5678::", "2001:cdba:9abc:5678::" },
+ { "::2001:cdba:9abc:5678", "::2001:cdba:9abc:5678" },
+ { "64:ff9b::1.2.3.4", "64:ff9b::102:304" },
+
+ { "::9abc:5678", "::154.188.86.120" },
+
+ // Mapped IPv4
+ { "::ffff:127.0.0.1", "127.0.0.1" },
+
+ // Android does not recognize Octal (leading 0) cases: they are treated as decimal.
+ { "0177.00.00.01", "177.0.0.1" },
+
+ // Verify that examples from JavaDoc work correctly.
+ { "192.0.2.1", "192.0.2.1" },
+ { "2001:db8::1:2", "2001:db8::1:2" },
+ };
+ }
+
+ public static String[] invalidNumericAddresses() {
+ return new String[] {
+ "",
+ " ",
+ "\t",
+ "\n",
+ "1.2.3.4.",
+ "1.2.3",
+ "1.2",
+ "1",
+ "1234",
+ "0",
+ "0x1.0x2.0x3.0x4",
+ "0x7f.0x00.0x00.0x01",
+ "0256.00.00.01",
+ "fred",
+ "www.google.com",
+ // IPv6 encoded for use in URL as defined in RFC 2732
+ "[fe80::6:2222]",
+ };
+ }
+
+ @Parameters(method = "validNumericAddressesAndStringRepresentation")
+ @Test
+ public void parseNumericAddress(String address, String expectedString) {
+ InetAddress inetAddress = InetAddresses.parseNumericAddress(address);
+ assertEquals(expectedString, inetAddress.getHostAddress());
+ }
+
+ @Parameters(method = "invalidNumericAddresses")
+ @Test
+ public void test_parseNonNumericAddress(String address) {
+ try {
+ InetAddress inetAddress = InetAddresses.parseNumericAddress(address);
+ fail(String.format(
+ "Address %s is not numeric but was parsed as %s", address, inetAddress));
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage()).contains(address);
+ }
+ }
+
+ @Test
+ public void test_parseNumericAddress_null() {
+ try {
+ InetAddress inetAddress = InetAddresses.parseNumericAddress(null);
+ fail(String.format("null is not numeric but was parsed as %s", inetAddress));
+ } catch (NullPointerException e) {
+ // expected
+ }
+ }
+
+ @Parameters(method = "validNumericAddressesAndStringRepresentation")
+ @Test
+ public void test_isNumericAddress(String address, String unused) {
+ assertTrue("expected '" + address + "' to be treated as numeric",
+ InetAddresses.isNumericAddress(address));
+ }
+
+ @Parameters(method = "invalidNumericAddresses")
+ @Test
+ public void test_isNotNumericAddress(String address) {
+ assertFalse("expected '" + address + "' to be treated as non-numeric",
+ InetAddresses.isNumericAddress(address));
+ }
+
+ @Test
+ public void test_isNumericAddress_null() {
+ try {
+ InetAddresses.isNumericAddress(null);
+ fail("expected null to throw a NullPointerException");
+ } catch (NullPointerException e) {
+ // expected
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/IpConfigurationTest.java b/tests/cts/net/src/android/net/cts/IpConfigurationTest.java
new file mode 100644
index 0000000..56ab2a7
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpConfigurationTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2019 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 static com.android.testutils.ParcelUtils.assertParcelSane;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.net.IpConfiguration;
+import android.net.LinkAddress;
+import android.net.ProxyInfo;
+import android.net.StaticIpConfiguration;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.net.InetAddressUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
+public final class IpConfigurationTest {
+ private static final LinkAddress LINKADDR = new LinkAddress("192.0.2.2/25");
+ private static final InetAddress GATEWAY = InetAddressUtils.parseNumericAddress("192.0.2.1");
+ private static final InetAddress DNS1 = InetAddressUtils.parseNumericAddress("8.8.8.8");
+ private static final InetAddress DNS2 = InetAddressUtils.parseNumericAddress("8.8.4.4");
+ private static final String DOMAINS = "example.com";
+
+ private static final ArrayList<InetAddress> dnsServers = new ArrayList<>();
+
+ private StaticIpConfiguration mStaticIpConfig;
+ private ProxyInfo mProxy;
+
+ @Before
+ public void setUp() {
+ dnsServers.add(DNS1);
+ dnsServers.add(DNS2);
+ mStaticIpConfig = new StaticIpConfiguration.Builder()
+ .setIpAddress(LINKADDR)
+ .setGateway(GATEWAY)
+ .setDnsServers(dnsServers)
+ .setDomains(DOMAINS)
+ .build();
+
+ mProxy = ProxyInfo.buildDirectProxy("test", 8888);
+ }
+
+ @Test
+ public void testConstructor() {
+ IpConfiguration ipConfig = new IpConfiguration();
+ checkEmpty(ipConfig);
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration());
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
+
+ ipConfig.setStaticIpConfiguration(mStaticIpConfig);
+ ipConfig.setHttpProxy(mProxy);
+
+ ipConfig.setIpAssignment(IpConfiguration.IpAssignment.STATIC);
+ ipConfig.setProxySettings(IpConfiguration.ProxySettings.PAC);
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
+
+ ipConfig.setIpAssignment(IpConfiguration.IpAssignment.STATIC);
+ ipConfig.setProxySettings(IpConfiguration.ProxySettings.STATIC);
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
+
+ ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP);
+ ipConfig.setProxySettings(IpConfiguration.ProxySettings.PAC);
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
+
+ ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP);
+ ipConfig.setProxySettings(IpConfiguration.ProxySettings.PAC);
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
+
+ ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP);
+ ipConfig.setProxySettings(IpConfiguration.ProxySettings.STATIC);
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
+
+ ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP);
+ ipConfig.setProxySettings(IpConfiguration.ProxySettings.NONE);
+ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig));
+ }
+
+ private void checkEmpty(IpConfiguration config) {
+ assertEquals(IpConfiguration.IpAssignment.UNASSIGNED,
+ config.getIpAssignment().UNASSIGNED);
+ assertEquals(IpConfiguration.ProxySettings.UNASSIGNED,
+ config.getProxySettings().UNASSIGNED);
+ assertNull(config.getStaticIpConfiguration());
+ assertNull(config.getHttpProxy());
+ }
+
+ private void assertIpConfigurationEqual(IpConfiguration source, IpConfiguration target) {
+ assertEquals(source.getIpAssignment(), target.getIpAssignment());
+ assertEquals(source.getProxySettings(), target.getProxySettings());
+ assertEquals(source.getHttpProxy(), target.getHttpProxy());
+ assertEquals(source.getStaticIpConfiguration(), target.getStaticIpConfiguration());
+ }
+
+ @Test
+ public void testParcel() {
+ final IpConfiguration config = new IpConfiguration();
+ assertParcelSane(config, 4);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
new file mode 100644
index 0000000..10e43e7
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
@@ -0,0 +1,556 @@
+/*
+ * Copyright (C) 2018 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 static org.junit.Assert.assertArrayEquals;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.IpSecAlgorithm;
+import android.net.IpSecManager;
+import android.net.IpSecTransform;
+import android.platform.test.annotations.AppModeFull;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class IpSecBaseTest {
+
+ private static final String TAG = IpSecBaseTest.class.getSimpleName();
+
+ protected static final String IPV4_LOOPBACK = "127.0.0.1";
+ protected static final String IPV6_LOOPBACK = "::1";
+ protected static final String[] LOOPBACK_ADDRS = new String[] {IPV4_LOOPBACK, IPV6_LOOPBACK};
+ protected static final int[] DIRECTIONS =
+ new int[] {IpSecManager.DIRECTION_IN, IpSecManager.DIRECTION_OUT};
+
+ protected static final byte[] TEST_DATA = "Best test data ever!".getBytes();
+ protected static final int DATA_BUFFER_LEN = 4096;
+ protected static final int SOCK_TIMEOUT = 500;
+
+ private static final byte[] KEY_DATA = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+ 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
+ 0x20, 0x21, 0x22, 0x23
+ };
+
+ protected static final byte[] AUTH_KEY = getKey(256);
+ protected static final byte[] CRYPT_KEY = getKey(256);
+
+ protected ConnectivityManager mCM;
+ protected IpSecManager mISM;
+
+ @Before
+ public void setUp() throws Exception {
+ mISM =
+ (IpSecManager)
+ InstrumentationRegistry.getContext()
+ .getSystemService(Context.IPSEC_SERVICE);
+ mCM =
+ (ConnectivityManager)
+ InstrumentationRegistry.getContext()
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ protected static byte[] getKey(int bitLength) {
+ return Arrays.copyOf(KEY_DATA, bitLength / 8);
+ }
+
+ protected static int getDomain(InetAddress address) {
+ int domain;
+ if (address instanceof Inet6Address) {
+ domain = OsConstants.AF_INET6;
+ } else {
+ domain = OsConstants.AF_INET;
+ }
+ return domain;
+ }
+
+ protected static int getPort(FileDescriptor sock) throws Exception {
+ return ((InetSocketAddress) Os.getsockname(sock)).getPort();
+ }
+
+ public static interface GenericSocket extends AutoCloseable {
+ void send(byte[] data) throws Exception;
+
+ byte[] receive() throws Exception;
+
+ int getPort() throws Exception;
+
+ void close() throws Exception;
+
+ void applyTransportModeTransform(
+ IpSecManager ism, int direction, IpSecTransform transform) throws Exception;
+
+ void removeTransportModeTransforms(IpSecManager ism) throws Exception;
+ }
+
+ public static interface GenericTcpSocket extends GenericSocket {}
+
+ public static interface GenericUdpSocket extends GenericSocket {
+ void sendTo(byte[] data, InetAddress dstAddr, int port) throws Exception;
+ }
+
+ public abstract static class NativeSocket implements GenericSocket {
+ public FileDescriptor mFd;
+
+ public NativeSocket(FileDescriptor fd) {
+ mFd = fd;
+ }
+
+ @Override
+ public void send(byte[] data) throws Exception {
+ Os.write(mFd, data, 0, data.length);
+ }
+
+ @Override
+ public byte[] receive() throws Exception {
+ byte[] in = new byte[DATA_BUFFER_LEN];
+ AtomicInteger bytesRead = new AtomicInteger(-1);
+
+ Thread readSockThread = new Thread(() -> {
+ long startTime = System.currentTimeMillis();
+ while (bytesRead.get() < 0 && System.currentTimeMillis() < startTime + SOCK_TIMEOUT) {
+ try {
+ bytesRead.set(Os.recvfrom(mFd, in, 0, DATA_BUFFER_LEN, 0, null));
+ } catch (Exception e) {
+ Log.e(TAG, "Error encountered reading from socket", e);
+ }
+ }
+ });
+
+ readSockThread.start();
+ readSockThread.join(SOCK_TIMEOUT);
+
+ if (bytesRead.get() < 0) {
+ throw new IOException("No data received from socket");
+ }
+
+ return Arrays.copyOfRange(in, 0, bytesRead.get());
+ }
+
+ @Override
+ public int getPort() throws Exception {
+ return IpSecBaseTest.getPort(mFd);
+ }
+
+ @Override
+ public void close() throws Exception {
+ Os.close(mFd);
+ }
+
+ @Override
+ public void applyTransportModeTransform(
+ IpSecManager ism, int direction, IpSecTransform transform) throws Exception {
+ ism.applyTransportModeTransform(mFd, direction, transform);
+ }
+
+ @Override
+ public void removeTransportModeTransforms(IpSecManager ism) throws Exception {
+ ism.removeTransportModeTransforms(mFd);
+ }
+ }
+
+ public static class NativeTcpSocket extends NativeSocket implements GenericTcpSocket {
+ public NativeTcpSocket(FileDescriptor fd) {
+ super(fd);
+ }
+ }
+
+ public static class NativeUdpSocket extends NativeSocket implements GenericUdpSocket {
+ public NativeUdpSocket(FileDescriptor fd) {
+ super(fd);
+ }
+
+ @Override
+ public void sendTo(byte[] data, InetAddress dstAddr, int port) throws Exception {
+ Os.sendto(mFd, data, 0, data.length, 0, dstAddr, port);
+ }
+ }
+
+ public static class JavaUdpSocket implements GenericUdpSocket {
+ public final DatagramSocket mSocket;
+
+ public JavaUdpSocket(InetAddress localAddr, int port) {
+ try {
+ mSocket = new DatagramSocket(port, localAddr);
+ mSocket.setSoTimeout(SOCK_TIMEOUT);
+ } catch (SocketException e) {
+ // Fail loudly if we can't set up sockets properly. And without the timeout, we
+ // could easily end up in an endless wait.
+ throw new RuntimeException(e);
+ }
+ }
+
+ public JavaUdpSocket(InetAddress localAddr) {
+ try {
+ mSocket = new DatagramSocket(0, localAddr);
+ mSocket.setSoTimeout(SOCK_TIMEOUT);
+ } catch (SocketException e) {
+ // Fail loudly if we can't set up sockets properly. And without the timeout, we
+ // could easily end up in an endless wait.
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void send(byte[] data) throws Exception {
+ mSocket.send(new DatagramPacket(data, data.length));
+ }
+
+ @Override
+ public void sendTo(byte[] data, InetAddress dstAddr, int port) throws Exception {
+ mSocket.send(new DatagramPacket(data, data.length, dstAddr, port));
+ }
+
+ @Override
+ public int getPort() throws Exception {
+ return mSocket.getLocalPort();
+ }
+
+ @Override
+ public void close() throws Exception {
+ mSocket.close();
+ }
+
+ @Override
+ public byte[] receive() throws Exception {
+ DatagramPacket data = new DatagramPacket(new byte[DATA_BUFFER_LEN], DATA_BUFFER_LEN);
+ mSocket.receive(data);
+ return Arrays.copyOfRange(data.getData(), 0, data.getLength());
+ }
+
+ @Override
+ public void applyTransportModeTransform(
+ IpSecManager ism, int direction, IpSecTransform transform) throws Exception {
+ ism.applyTransportModeTransform(mSocket, direction, transform);
+ }
+
+ @Override
+ public void removeTransportModeTransforms(IpSecManager ism) throws Exception {
+ ism.removeTransportModeTransforms(mSocket);
+ }
+ }
+
+ public static class JavaTcpSocket implements GenericTcpSocket {
+ public final Socket mSocket;
+
+ public JavaTcpSocket(Socket socket) {
+ mSocket = socket;
+ try {
+ mSocket.setSoTimeout(SOCK_TIMEOUT);
+ } catch (SocketException e) {
+ // Fail loudly if we can't set up sockets properly. And without the timeout, we
+ // could easily end up in an endless wait.
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void send(byte[] data) throws Exception {
+ mSocket.getOutputStream().write(data);
+ }
+
+ @Override
+ public byte[] receive() throws Exception {
+ byte[] in = new byte[DATA_BUFFER_LEN];
+ int bytesRead = mSocket.getInputStream().read(in);
+ return Arrays.copyOfRange(in, 0, bytesRead);
+ }
+
+ @Override
+ public int getPort() throws Exception {
+ return mSocket.getLocalPort();
+ }
+
+ @Override
+ public void close() throws Exception {
+ mSocket.close();
+ }
+
+ @Override
+ public void applyTransportModeTransform(
+ IpSecManager ism, int direction, IpSecTransform transform) throws Exception {
+ ism.applyTransportModeTransform(mSocket, direction, transform);
+ }
+
+ @Override
+ public void removeTransportModeTransforms(IpSecManager ism) throws Exception {
+ ism.removeTransportModeTransforms(mSocket);
+ }
+ }
+
+ public static class SocketPair<T> {
+ public final T mLeftSock;
+ public final T mRightSock;
+
+ public SocketPair(T leftSock, T rightSock) {
+ mLeftSock = leftSock;
+ mRightSock = rightSock;
+ }
+ }
+
+ protected static void applyTransformBidirectionally(
+ IpSecManager ism, IpSecTransform transform, GenericSocket socket) throws Exception {
+ for (int direction : DIRECTIONS) {
+ socket.applyTransportModeTransform(ism, direction, transform);
+ }
+ }
+
+ public static SocketPair<NativeUdpSocket> getNativeUdpSocketPair(
+ InetAddress localAddr, IpSecManager ism, IpSecTransform transform, boolean connected)
+ throws Exception {
+ int domain = getDomain(localAddr);
+
+ NativeUdpSocket leftSock = new NativeUdpSocket(
+ Os.socket(domain, OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP));
+ NativeUdpSocket rightSock = new NativeUdpSocket(
+ Os.socket(domain, OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP));
+
+ for (NativeUdpSocket sock : new NativeUdpSocket[] {leftSock, rightSock}) {
+ applyTransformBidirectionally(ism, transform, sock);
+ Os.bind(sock.mFd, localAddr, 0);
+ }
+
+ if (connected) {
+ Os.connect(leftSock.mFd, localAddr, rightSock.getPort());
+ Os.connect(rightSock.mFd, localAddr, leftSock.getPort());
+ }
+
+ return new SocketPair<>(leftSock, rightSock);
+ }
+
+ public static SocketPair<NativeTcpSocket> getNativeTcpSocketPair(
+ InetAddress localAddr, IpSecManager ism, IpSecTransform transform) throws Exception {
+ int domain = getDomain(localAddr);
+
+ NativeTcpSocket server = new NativeTcpSocket(
+ Os.socket(domain, OsConstants.SOCK_STREAM, OsConstants.IPPROTO_TCP));
+ NativeTcpSocket client = new NativeTcpSocket(
+ Os.socket(domain, OsConstants.SOCK_STREAM, OsConstants.IPPROTO_TCP));
+
+ Os.bind(server.mFd, localAddr, 0);
+
+ applyTransformBidirectionally(ism, transform, server);
+ applyTransformBidirectionally(ism, transform, client);
+
+ Os.listen(server.mFd, 10);
+ Os.connect(client.mFd, localAddr, server.getPort());
+ NativeTcpSocket accepted = new NativeTcpSocket(Os.accept(server.mFd, null));
+
+ applyTransformBidirectionally(ism, transform, accepted);
+ server.close();
+
+ return new SocketPair<>(client, accepted);
+ }
+
+ public static SocketPair<JavaUdpSocket> getJavaUdpSocketPair(
+ InetAddress localAddr, IpSecManager ism, IpSecTransform transform, boolean connected)
+ throws Exception {
+ JavaUdpSocket leftSock = new JavaUdpSocket(localAddr);
+ JavaUdpSocket rightSock = new JavaUdpSocket(localAddr);
+
+ applyTransformBidirectionally(ism, transform, leftSock);
+ applyTransformBidirectionally(ism, transform, rightSock);
+
+ if (connected) {
+ leftSock.mSocket.connect(localAddr, rightSock.mSocket.getLocalPort());
+ rightSock.mSocket.connect(localAddr, leftSock.mSocket.getLocalPort());
+ }
+
+ return new SocketPair<>(leftSock, rightSock);
+ }
+
+ public static SocketPair<JavaTcpSocket> getJavaTcpSocketPair(
+ InetAddress localAddr, IpSecManager ism, IpSecTransform transform) throws Exception {
+ JavaTcpSocket clientSock = new JavaTcpSocket(new Socket());
+ ServerSocket serverSocket = new ServerSocket();
+ serverSocket.bind(new InetSocketAddress(localAddr, 0));
+
+ // While technically the client socket does not need to be bound, the OpenJDK implementation
+ // of Socket only allocates an FD when bind() or connect() or other similar methods are
+ // called. So we call bind to force the FD creation, so that we can apply a transform to it
+ // prior to socket connect.
+ clientSock.mSocket.bind(new InetSocketAddress(localAddr, 0));
+
+ // IpSecService doesn't support serverSockets at the moment; workaround using FD
+ FileDescriptor serverFd = serverSocket.getImpl().getFD$();
+
+ applyTransformBidirectionally(ism, transform, new NativeTcpSocket(serverFd));
+ applyTransformBidirectionally(ism, transform, clientSock);
+
+ clientSock.mSocket.connect(new InetSocketAddress(localAddr, serverSocket.getLocalPort()));
+ JavaTcpSocket acceptedSock = new JavaTcpSocket(serverSocket.accept());
+
+ applyTransformBidirectionally(ism, transform, acceptedSock);
+ serverSocket.close();
+
+ return new SocketPair<>(clientSock, acceptedSock);
+ }
+
+ private void checkSocketPair(GenericSocket left, GenericSocket right) throws Exception {
+ left.send(TEST_DATA);
+ assertArrayEquals(TEST_DATA, right.receive());
+
+ right.send(TEST_DATA);
+ assertArrayEquals(TEST_DATA, left.receive());
+
+ left.close();
+ right.close();
+ }
+
+ private void checkUnconnectedUdpSocketPair(
+ GenericUdpSocket left, GenericUdpSocket right, InetAddress localAddr) throws Exception {
+ left.sendTo(TEST_DATA, localAddr, right.getPort());
+ assertArrayEquals(TEST_DATA, right.receive());
+
+ right.sendTo(TEST_DATA, localAddr, left.getPort());
+ assertArrayEquals(TEST_DATA, left.receive());
+
+ left.close();
+ right.close();
+ }
+
+ protected static IpSecTransform buildIpSecTransform(
+ Context context,
+ IpSecManager.SecurityParameterIndex spi,
+ IpSecManager.UdpEncapsulationSocket encapSocket,
+ InetAddress remoteAddr)
+ throws Exception {
+ IpSecTransform.Builder builder =
+ new IpSecTransform.Builder(context)
+ .setEncryption(new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY))
+ .setAuthentication(
+ new IpSecAlgorithm(
+ IpSecAlgorithm.AUTH_HMAC_SHA256,
+ AUTH_KEY,
+ AUTH_KEY.length * 4));
+
+ if (encapSocket != null) {
+ builder.setIpv4Encapsulation(encapSocket, encapSocket.getPort());
+ }
+
+ return builder.buildTransportModeTransform(remoteAddr, spi);
+ }
+
+ private IpSecTransform buildDefaultTransform(InetAddress localAddr) throws Exception {
+ try (IpSecManager.SecurityParameterIndex spi =
+ mISM.allocateSecurityParameterIndex(localAddr)) {
+ return buildIpSecTransform(InstrumentationRegistry.getContext(), spi, null, localAddr);
+ }
+ }
+
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testJavaTcpSocketPair() throws Exception {
+ for (String addr : LOOPBACK_ADDRS) {
+ InetAddress local = InetAddress.getByName(addr);
+ try (IpSecTransform transform = buildDefaultTransform(local)) {
+ SocketPair<JavaTcpSocket> sockets = getJavaTcpSocketPair(local, mISM, transform);
+ checkSocketPair(sockets.mLeftSock, sockets.mRightSock);
+ }
+ }
+ }
+
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testJavaUdpSocketPair() throws Exception {
+ for (String addr : LOOPBACK_ADDRS) {
+ InetAddress local = InetAddress.getByName(addr);
+ try (IpSecTransform transform = buildDefaultTransform(local)) {
+ SocketPair<JavaUdpSocket> sockets =
+ getJavaUdpSocketPair(local, mISM, transform, true);
+ checkSocketPair(sockets.mLeftSock, sockets.mRightSock);
+ }
+ }
+ }
+
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testJavaUdpSocketPairUnconnected() throws Exception {
+ for (String addr : LOOPBACK_ADDRS) {
+ InetAddress local = InetAddress.getByName(addr);
+ try (IpSecTransform transform = buildDefaultTransform(local)) {
+ SocketPair<JavaUdpSocket> sockets =
+ getJavaUdpSocketPair(local, mISM, transform, false);
+ checkUnconnectedUdpSocketPair(sockets.mLeftSock, sockets.mRightSock, local);
+ }
+ }
+ }
+
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testNativeTcpSocketPair() throws Exception {
+ for (String addr : LOOPBACK_ADDRS) {
+ InetAddress local = InetAddress.getByName(addr);
+ try (IpSecTransform transform = buildDefaultTransform(local)) {
+ SocketPair<NativeTcpSocket> sockets =
+ getNativeTcpSocketPair(local, mISM, transform);
+ checkSocketPair(sockets.mLeftSock, sockets.mRightSock);
+ }
+ }
+ }
+
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testNativeUdpSocketPair() throws Exception {
+ for (String addr : LOOPBACK_ADDRS) {
+ InetAddress local = InetAddress.getByName(addr);
+ try (IpSecTransform transform = buildDefaultTransform(local)) {
+ SocketPair<NativeUdpSocket> sockets =
+ getNativeUdpSocketPair(local, mISM, transform, true);
+ checkSocketPair(sockets.mLeftSock, sockets.mRightSock);
+ }
+ }
+ }
+
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testNativeUdpSocketPairUnconnected() throws Exception {
+ for (String addr : LOOPBACK_ADDRS) {
+ InetAddress local = InetAddress.getByName(addr);
+ try (IpSecTransform transform = buildDefaultTransform(local)) {
+ SocketPair<NativeUdpSocket> sockets =
+ getNativeUdpSocketPair(local, mISM, transform, false);
+ checkUnconnectedUdpSocketPair(sockets.mLeftSock, sockets.mRightSock, local);
+ }
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
new file mode 100644
index 0000000..355b496
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -0,0 +1,1189 @@
+/*
+ * Copyright (C) 2017 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 static android.net.cts.PacketUtils.AES_CBC_BLK_SIZE;
+import static android.net.cts.PacketUtils.AES_CBC_IV_LEN;
+import static android.net.cts.PacketUtils.AES_GCM_BLK_SIZE;
+import static android.net.cts.PacketUtils.AES_GCM_IV_LEN;
+import static android.net.cts.PacketUtils.IP4_HDRLEN;
+import static android.net.cts.PacketUtils.IP6_HDRLEN;
+import static android.net.cts.PacketUtils.TCP_HDRLEN_WITH_TIMESTAMP_OPT;
+import static android.net.cts.PacketUtils.UDP_HDRLEN;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.IpSecAlgorithm;
+import android.net.IpSecManager;
+import android.net.IpSecTransform;
+import android.net.TrafficStats;
+import android.platform.test.annotations.AppModeFull;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.Arrays;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Socket cannot bind in instant app mode")
+public class IpSecManagerTest extends IpSecBaseTest {
+
+ private static final String TAG = IpSecManagerTest.class.getSimpleName();
+
+ private static final InetAddress GOOGLE_DNS_4 = InetAddress.parseNumericAddress("8.8.8.8");
+ private static final InetAddress GOOGLE_DNS_6 =
+ InetAddress.parseNumericAddress("2001:4860:4860::8888");
+
+ private static final InetAddress[] GOOGLE_DNS_LIST =
+ new InetAddress[] {GOOGLE_DNS_4, GOOGLE_DNS_6};
+
+ private static final int DROID_SPI = 0xD1201D;
+ private static final int MAX_PORT_BIND_ATTEMPTS = 10;
+
+ private static final byte[] AEAD_KEY = getKey(288);
+
+ /*
+ * Allocate a random SPI
+ * Allocate a specific SPI using previous randomly created SPI value
+ * Realloc the same SPI that was specifically created (expect SpiUnavailable)
+ * Close SPIs
+ */
+ @Test
+ public void testAllocSpi() throws Exception {
+ for (InetAddress addr : GOOGLE_DNS_LIST) {
+ IpSecManager.SecurityParameterIndex randomSpi = null, droidSpi = null;
+ randomSpi = mISM.allocateSecurityParameterIndex(addr);
+ assertTrue(
+ "Failed to receive a valid SPI",
+ randomSpi.getSpi() != IpSecManager.INVALID_SECURITY_PARAMETER_INDEX);
+
+ droidSpi = mISM.allocateSecurityParameterIndex(addr, DROID_SPI);
+ assertTrue("Failed to allocate specified SPI, " + DROID_SPI,
+ droidSpi.getSpi() == DROID_SPI);
+
+ try {
+ mISM.allocateSecurityParameterIndex(addr, DROID_SPI);
+ fail("Duplicate SPI was allowed to be created");
+ } catch (IpSecManager.SpiUnavailableException expected) {
+ // This is a success case because we expect a dupe SPI to throw
+ }
+
+ randomSpi.close();
+ droidSpi.close();
+ }
+ }
+
+ /** This function finds an available port */
+ private static int findUnusedPort() throws Exception {
+ // Get an available port.
+ DatagramSocket s = new DatagramSocket();
+ int port = s.getLocalPort();
+ s.close();
+ return port;
+ }
+
+ private static FileDescriptor getBoundUdpSocket(InetAddress address) throws Exception {
+ FileDescriptor sock =
+ Os.socket(getDomain(address), OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP);
+
+ for (int i = 0; i < MAX_PORT_BIND_ATTEMPTS; i++) {
+ try {
+ int port = findUnusedPort();
+ Os.bind(sock, address, port);
+ break;
+ } catch (ErrnoException e) {
+ // Someone claimed the port since we called findUnusedPort.
+ if (e.errno == OsConstants.EADDRINUSE) {
+ if (i == MAX_PORT_BIND_ATTEMPTS - 1) {
+
+ fail("Failed " + MAX_PORT_BIND_ATTEMPTS + " attempts to bind to a port");
+ }
+ continue;
+ }
+ throw e.rethrowAsIOException();
+ }
+ }
+ return sock;
+ }
+
+ private void checkUnconnectedUdp(IpSecTransform transform, InetAddress local, int sendCount,
+ boolean useJavaSockets) throws Exception {
+ GenericUdpSocket sockLeft = null, sockRight = null;
+ if (useJavaSockets) {
+ SocketPair<JavaUdpSocket> sockets = getJavaUdpSocketPair(local, mISM, transform, false);
+ sockLeft = sockets.mLeftSock;
+ sockRight = sockets.mRightSock;
+ } else {
+ SocketPair<NativeUdpSocket> sockets =
+ getNativeUdpSocketPair(local, mISM, transform, false);
+ sockLeft = sockets.mLeftSock;
+ sockRight = sockets.mRightSock;
+ }
+
+ for (int i = 0; i < sendCount; i++) {
+ byte[] in;
+
+ sockLeft.sendTo(TEST_DATA, local, sockRight.getPort());
+ in = sockRight.receive();
+ assertArrayEquals("Left-to-right encrypted data did not match.", TEST_DATA, in);
+
+ sockRight.sendTo(TEST_DATA, local, sockLeft.getPort());
+ in = sockLeft.receive();
+ assertArrayEquals("Right-to-left encrypted data did not match.", TEST_DATA, in);
+ }
+
+ sockLeft.close();
+ sockRight.close();
+ }
+
+ private void checkTcp(IpSecTransform transform, InetAddress local, int sendCount,
+ boolean useJavaSockets) throws Exception {
+ GenericTcpSocket client = null, accepted = null;
+ if (useJavaSockets) {
+ SocketPair<JavaTcpSocket> sockets = getJavaTcpSocketPair(local, mISM, transform);
+ client = sockets.mLeftSock;
+ accepted = sockets.mRightSock;
+ } else {
+ SocketPair<NativeTcpSocket> sockets = getNativeTcpSocketPair(local, mISM, transform);
+ client = sockets.mLeftSock;
+ accepted = sockets.mRightSock;
+ }
+
+ // Wait for TCP handshake packets to be counted
+ StatsChecker.waitForNumPackets(3); // (SYN, SYN+ACK, ACK)
+
+ // Reset StatsChecker, to ignore negotiation overhead.
+ StatsChecker.initStatsChecker();
+ for (int i = 0; i < sendCount; i++) {
+ byte[] in;
+
+ client.send(TEST_DATA);
+ in = accepted.receive();
+ assertArrayEquals("Client-to-server encrypted data did not match.", TEST_DATA, in);
+
+ // Allow for newest data + ack packets to be returned before sending next packet
+ // Also add the number of expected packets in each of the previous runs (4 per run)
+ StatsChecker.waitForNumPackets(2 + (4 * i));
+
+ accepted.send(TEST_DATA);
+ in = client.receive();
+ assertArrayEquals("Server-to-client encrypted data did not match.", TEST_DATA, in);
+
+ // Allow for all data + ack packets to be returned before sending next packet
+ // Also add the number of expected packets in each of the previous runs (4 per run)
+ StatsChecker.waitForNumPackets(4 * (i + 1));
+ }
+
+ // Transforms should not be removed from the sockets, otherwise FIN packets will be sent
+ // unencrypted.
+ // This test also unfortunately happens to rely on a nuance of the cleanup order. By
+ // keeping the policy on the socket, but removing the SA before lingering FIN packets
+ // are sent (at an undetermined later time), the FIN packets are dropped. Without this,
+ // we run into all kinds of headaches trying to test data accounting (unsolicited
+ // packets mysteriously appearing and messing up our counters)
+ // The right way to close sockets is to set SO_LINGER to ensure synchronous closure,
+ // closing the sockets, and then closing the transforms. See documentation for the
+ // Socket or FileDescriptor flavors of applyTransportModeTransform() in IpSecManager
+ // for more details.
+
+ client.close();
+ accepted.close();
+ }
+
+ /*
+ * Alloc outbound SPI
+ * Alloc inbound SPI
+ * Create transport mode transform
+ * open socket
+ * apply transform to socket
+ * send data on socket
+ * release transform
+ * send data (expect exception)
+ */
+ @Test
+ public void testCreateTransform() throws Exception {
+ InetAddress localAddr = InetAddress.getByName(IPV4_LOOPBACK);
+ IpSecManager.SecurityParameterIndex spi =
+ mISM.allocateSecurityParameterIndex(localAddr);
+
+ IpSecTransform transform =
+ new IpSecTransform.Builder(InstrumentationRegistry.getContext())
+ .setEncryption(new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY))
+ .setAuthentication(
+ new IpSecAlgorithm(
+ IpSecAlgorithm.AUTH_HMAC_SHA256,
+ AUTH_KEY,
+ AUTH_KEY.length * 8))
+ .buildTransportModeTransform(localAddr, spi);
+
+ final boolean [][] applyInApplyOut = {
+ {false, false}, {false, true}, {true, false}, {true,true}};
+ final byte[] data = new String("Best test data ever!").getBytes("UTF-8");
+ final DatagramPacket outPacket = new DatagramPacket(data, 0, data.length, localAddr, 0);
+
+ byte[] in = new byte[data.length];
+ DatagramPacket inPacket = new DatagramPacket(in, in.length);
+ DatagramSocket localSocket;
+ int localPort;
+
+ for(boolean[] io : applyInApplyOut) {
+ boolean applyIn = io[0];
+ boolean applyOut = io[1];
+ // Bind localSocket to a random available port.
+ localSocket = new DatagramSocket(0);
+ localPort = localSocket.getLocalPort();
+ localSocket.setSoTimeout(200);
+ outPacket.setPort(localPort);
+ if (applyIn) {
+ mISM.applyTransportModeTransform(
+ localSocket, IpSecManager.DIRECTION_IN, transform);
+ }
+ if (applyOut) {
+ mISM.applyTransportModeTransform(
+ localSocket, IpSecManager.DIRECTION_OUT, transform);
+ }
+ if (applyIn == applyOut) {
+ localSocket.send(outPacket);
+ localSocket.receive(inPacket);
+ assertTrue("Encapsulated data did not match.",
+ Arrays.equals(outPacket.getData(), inPacket.getData()));
+ mISM.removeTransportModeTransforms(localSocket);
+ localSocket.close();
+ } else {
+ try {
+ localSocket.send(outPacket);
+ localSocket.receive(inPacket);
+ } catch (IOException e) {
+ continue;
+ } finally {
+ mISM.removeTransportModeTransforms(localSocket);
+ localSocket.close();
+ }
+ // FIXME: This check is disabled because sockets currently receive data
+ // if there is a valid SA for decryption, even when the input policy is
+ // not applied to a socket.
+ // fail("Data IO should fail on asymmetrical transforms! + Input="
+ // + applyIn + " Output=" + applyOut);
+ }
+ }
+ transform.close();
+ }
+
+ /** Snapshot of TrafficStats as of initStatsChecker call for later comparisons */
+ private static class StatsChecker {
+ private static final double ERROR_MARGIN_BYTES = 1.05;
+ private static final double ERROR_MARGIN_PKTS = 1.05;
+ private static final int MAX_WAIT_TIME_MILLIS = 1000;
+
+ private static long uidTxBytes;
+ private static long uidRxBytes;
+ private static long uidTxPackets;
+ private static long uidRxPackets;
+
+ private static long ifaceTxBytes;
+ private static long ifaceRxBytes;
+ private static long ifaceTxPackets;
+ private static long ifaceRxPackets;
+
+ /**
+ * This method counts the number of incoming packets, polling intermittently up to
+ * MAX_WAIT_TIME_MILLIS.
+ */
+ private static void waitForNumPackets(int numPackets) throws Exception {
+ long uidTxDelta = 0;
+ long uidRxDelta = 0;
+ for (int i = 0; i < 100; i++) {
+ uidTxDelta = TrafficStats.getUidTxPackets(Os.getuid()) - uidTxPackets;
+ uidRxDelta = TrafficStats.getUidRxPackets(Os.getuid()) - uidRxPackets;
+
+ // TODO: Check Rx packets as well once kernel security policy bug is fixed.
+ // (b/70635417)
+ if (uidTxDelta >= numPackets) {
+ return;
+ }
+ Thread.sleep(MAX_WAIT_TIME_MILLIS / 100);
+ }
+ fail(
+ "Not enough traffic was recorded to satisfy the provided conditions: wanted "
+ + numPackets
+ + ", got "
+ + uidTxDelta
+ + " tx and "
+ + uidRxDelta
+ + " rx packets");
+ }
+
+ private static void assertUidStatsDelta(
+ int expectedTxByteDelta,
+ int expectedTxPacketDelta,
+ int minRxByteDelta,
+ int maxRxByteDelta,
+ int expectedRxPacketDelta) {
+ long newUidTxBytes = TrafficStats.getUidTxBytes(Os.getuid());
+ long newUidRxBytes = TrafficStats.getUidRxBytes(Os.getuid());
+ long newUidTxPackets = TrafficStats.getUidTxPackets(Os.getuid());
+ long newUidRxPackets = TrafficStats.getUidRxPackets(Os.getuid());
+
+ assertEquals(expectedTxByteDelta, newUidTxBytes - uidTxBytes);
+ assertTrue(
+ newUidRxBytes - uidRxBytes >= minRxByteDelta
+ && newUidRxBytes - uidRxBytes <= maxRxByteDelta);
+ assertEquals(expectedTxPacketDelta, newUidTxPackets - uidTxPackets);
+ assertEquals(expectedRxPacketDelta, newUidRxPackets - uidRxPackets);
+ }
+
+ private static void assertIfaceStatsDelta(
+ int expectedTxByteDelta,
+ int expectedTxPacketDelta,
+ int expectedRxByteDelta,
+ int expectedRxPacketDelta)
+ throws IOException {
+ long newIfaceTxBytes = TrafficStats.getLoopbackTxBytes();
+ long newIfaceRxBytes = TrafficStats.getLoopbackRxBytes();
+ long newIfaceTxPackets = TrafficStats.getLoopbackTxPackets();
+ long newIfaceRxPackets = TrafficStats.getLoopbackRxPackets();
+
+ // Check that iface stats are within an acceptable range; data might be sent
+ // on the local interface by other apps.
+ assertApproxEquals(
+ ifaceTxBytes, newIfaceTxBytes, expectedTxByteDelta, ERROR_MARGIN_BYTES);
+ assertApproxEquals(
+ ifaceRxBytes, newIfaceRxBytes, expectedRxByteDelta, ERROR_MARGIN_BYTES);
+ assertApproxEquals(
+ ifaceTxPackets, newIfaceTxPackets, expectedTxPacketDelta, ERROR_MARGIN_PKTS);
+ assertApproxEquals(
+ ifaceRxPackets, newIfaceRxPackets, expectedRxPacketDelta, ERROR_MARGIN_PKTS);
+ }
+
+ private static void assertApproxEquals(
+ long oldStats, long newStats, int expectedDelta, double errorMargin) {
+ assertTrue(expectedDelta <= newStats - oldStats);
+ assertTrue((expectedDelta * errorMargin) > newStats - oldStats);
+ }
+
+ private static void initStatsChecker() throws Exception {
+ uidTxBytes = TrafficStats.getUidTxBytes(Os.getuid());
+ uidRxBytes = TrafficStats.getUidRxBytes(Os.getuid());
+ uidTxPackets = TrafficStats.getUidTxPackets(Os.getuid());
+ uidRxPackets = TrafficStats.getUidRxPackets(Os.getuid());
+
+ ifaceTxBytes = TrafficStats.getLoopbackTxBytes();
+ ifaceRxBytes = TrafficStats.getLoopbackRxBytes();
+ ifaceTxPackets = TrafficStats.getLoopbackTxPackets();
+ ifaceRxPackets = TrafficStats.getLoopbackRxPackets();
+ }
+ }
+
+ private int getTruncLenBits(IpSecAlgorithm authOrAead) {
+ return authOrAead == null ? 0 : authOrAead.getTruncationLengthBits();
+ }
+
+ private int getIvLen(IpSecAlgorithm cryptOrAead) {
+ if (cryptOrAead == null) { return 0; }
+
+ switch (cryptOrAead.getName()) {
+ case IpSecAlgorithm.CRYPT_AES_CBC:
+ return AES_CBC_IV_LEN;
+ case IpSecAlgorithm.AUTH_CRYPT_AES_GCM:
+ return AES_GCM_IV_LEN;
+ default:
+ throw new IllegalArgumentException(
+ "IV length unknown for algorithm" + cryptOrAead.getName());
+ }
+ }
+
+ private int getBlkSize(IpSecAlgorithm cryptOrAead) {
+ // RFC 4303, section 2.4 states that ciphertext plus pad_len, next_header fields must
+ // terminate on a 4-byte boundary. Thus, the minimum ciphertext block size is 4 bytes.
+ if (cryptOrAead == null) { return 4; }
+
+ switch (cryptOrAead.getName()) {
+ case IpSecAlgorithm.CRYPT_AES_CBC:
+ return AES_CBC_BLK_SIZE;
+ case IpSecAlgorithm.AUTH_CRYPT_AES_GCM:
+ return AES_GCM_BLK_SIZE;
+ default:
+ throw new IllegalArgumentException(
+ "Blk size unknown for algorithm" + cryptOrAead.getName());
+ }
+ }
+
+ public void checkTransform(
+ int protocol,
+ String localAddress,
+ IpSecAlgorithm crypt,
+ IpSecAlgorithm auth,
+ IpSecAlgorithm aead,
+ boolean doUdpEncap,
+ int sendCount,
+ boolean useJavaSockets)
+ throws Exception {
+ StatsChecker.initStatsChecker();
+ InetAddress local = InetAddress.getByName(localAddress);
+
+ try (IpSecManager.UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket();
+ IpSecManager.SecurityParameterIndex spi =
+ mISM.allocateSecurityParameterIndex(local)) {
+
+ IpSecTransform.Builder transformBuilder =
+ new IpSecTransform.Builder(InstrumentationRegistry.getContext());
+ if (crypt != null) {
+ transformBuilder.setEncryption(crypt);
+ }
+ if (auth != null) {
+ transformBuilder.setAuthentication(auth);
+ }
+ if (aead != null) {
+ transformBuilder.setAuthenticatedEncryption(aead);
+ }
+
+ if (doUdpEncap) {
+ transformBuilder =
+ transformBuilder.setIpv4Encapsulation(encapSocket, encapSocket.getPort());
+ }
+
+ int ipHdrLen = local instanceof Inet6Address ? IP6_HDRLEN : IP4_HDRLEN;
+ int transportHdrLen = 0;
+ int udpEncapLen = doUdpEncap ? UDP_HDRLEN : 0;
+
+ try (IpSecTransform transform =
+ transformBuilder.buildTransportModeTransform(local, spi)) {
+ if (protocol == IPPROTO_TCP) {
+ transportHdrLen = TCP_HDRLEN_WITH_TIMESTAMP_OPT;
+ checkTcp(transform, local, sendCount, useJavaSockets);
+ } else if (protocol == IPPROTO_UDP) {
+ transportHdrLen = UDP_HDRLEN;
+
+ // TODO: Also check connected udp.
+ checkUnconnectedUdp(transform, local, sendCount, useJavaSockets);
+ } else {
+ throw new IllegalArgumentException("Invalid protocol");
+ }
+ }
+
+ checkStatsChecker(
+ protocol,
+ ipHdrLen,
+ transportHdrLen,
+ udpEncapLen,
+ sendCount,
+ getIvLen(crypt != null ? crypt : aead),
+ getBlkSize(crypt != null ? crypt : aead),
+ getTruncLenBits(auth != null ? auth : aead));
+ }
+ }
+
+ private void checkStatsChecker(
+ int protocol,
+ int ipHdrLen,
+ int transportHdrLen,
+ int udpEncapLen,
+ int sendCount,
+ int ivLen,
+ int blkSize,
+ int truncLenBits)
+ throws Exception {
+
+ int innerPacketSize = TEST_DATA.length + transportHdrLen + ipHdrLen;
+ int outerPacketSize =
+ PacketUtils.calculateEspPacketSize(
+ TEST_DATA.length + transportHdrLen, ivLen, blkSize, truncLenBits)
+ + udpEncapLen
+ + ipHdrLen;
+
+ int expectedOuterBytes = outerPacketSize * sendCount;
+ int expectedInnerBytes = innerPacketSize * sendCount;
+ int expectedPackets = sendCount;
+
+ // Each run sends two packets, one in each direction.
+ sendCount *= 2;
+ expectedOuterBytes *= 2;
+ expectedInnerBytes *= 2;
+ expectedPackets *= 2;
+
+ // Add TCP ACKs for data packets
+ if (protocol == IPPROTO_TCP) {
+ int encryptedTcpPktSize =
+ PacketUtils.calculateEspPacketSize(
+ TCP_HDRLEN_WITH_TIMESTAMP_OPT, ivLen, blkSize, truncLenBits);
+
+ // Add data packet ACKs
+ expectedOuterBytes += (encryptedTcpPktSize + udpEncapLen + ipHdrLen) * (sendCount);
+ expectedInnerBytes += (TCP_HDRLEN_WITH_TIMESTAMP_OPT + ipHdrLen) * (sendCount);
+ expectedPackets += sendCount;
+ }
+
+ StatsChecker.waitForNumPackets(expectedPackets);
+
+ // eBPF only counts inner packets, whereas xt_qtaguid counts outer packets. Allow both
+ StatsChecker.assertUidStatsDelta(
+ expectedOuterBytes,
+ expectedPackets,
+ expectedInnerBytes,
+ expectedOuterBytes,
+ expectedPackets);
+
+ // Unreliable at low numbers due to potential interference from other processes.
+ if (sendCount >= 1000) {
+ StatsChecker.assertIfaceStatsDelta(
+ expectedOuterBytes, expectedPackets, expectedOuterBytes, expectedPackets);
+ }
+ }
+
+ private void checkIkePacket(
+ NativeUdpSocket wrappedEncapSocket, InetAddress localAddr) throws Exception {
+ StatsChecker.initStatsChecker();
+
+ try (NativeUdpSocket remoteSocket = new NativeUdpSocket(getBoundUdpSocket(localAddr))) {
+
+ // Append IKE/ESP header - 4 bytes of SPI, 4 bytes of seq number, all zeroed out
+ // If the first four bytes are zero, assume non-ESP (IKE traffic)
+ byte[] dataWithEspHeader = new byte[TEST_DATA.length + 8];
+ System.arraycopy(TEST_DATA, 0, dataWithEspHeader, 8, TEST_DATA.length);
+
+ // Send the IKE packet from remoteSocket to wrappedEncapSocket. Since IKE packets
+ // are multiplexed over the socket, we expect them to appear on the encap socket
+ // (as opposed to being decrypted and received on the non-encap socket)
+ remoteSocket.sendTo(dataWithEspHeader, localAddr, wrappedEncapSocket.getPort());
+ byte[] in = wrappedEncapSocket.receive();
+ assertArrayEquals("Encapsulated data did not match.", dataWithEspHeader, in);
+
+ // Also test that the IKE socket can send data out.
+ wrappedEncapSocket.sendTo(dataWithEspHeader, localAddr, remoteSocket.getPort());
+ in = remoteSocket.receive();
+ assertArrayEquals("Encapsulated data did not match.", dataWithEspHeader, in);
+
+ // Calculate expected packet sizes. Always use IPv4 header, since our kernels only
+ // guarantee support of UDP encap on IPv4.
+ int expectedNumPkts = 2;
+ int expectedPacketSize =
+ expectedNumPkts * (dataWithEspHeader.length + UDP_HDRLEN + IP4_HDRLEN);
+
+ StatsChecker.waitForNumPackets(expectedNumPkts);
+ StatsChecker.assertUidStatsDelta(
+ expectedPacketSize,
+ expectedNumPkts,
+ expectedPacketSize,
+ expectedPacketSize,
+ expectedNumPkts);
+ StatsChecker.assertIfaceStatsDelta(
+ expectedPacketSize, expectedNumPkts, expectedPacketSize, expectedNumPkts);
+ }
+ }
+
+ @Test
+ public void testIkeOverUdpEncapSocket() throws Exception {
+ // IPv6 not supported for UDP-encap-ESP
+ InetAddress local = InetAddress.getByName(IPV4_LOOPBACK);
+ try (IpSecManager.UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket()) {
+ NativeUdpSocket wrappedEncapSocket =
+ new NativeUdpSocket(encapSocket.getFileDescriptor());
+ checkIkePacket(wrappedEncapSocket, local);
+
+ // Now try with a transform applied to a socket using this Encap socket
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+
+ try (IpSecManager.SecurityParameterIndex spi =
+ mISM.allocateSecurityParameterIndex(local);
+ IpSecTransform transform =
+ new IpSecTransform.Builder(InstrumentationRegistry.getContext())
+ .setEncryption(crypt)
+ .setAuthentication(auth)
+ .setIpv4Encapsulation(encapSocket, encapSocket.getPort())
+ .buildTransportModeTransform(local, spi);
+ JavaUdpSocket localSocket = new JavaUdpSocket(local)) {
+ applyTransformBidirectionally(mISM, transform, localSocket);
+
+ checkIkePacket(wrappedEncapSocket, local);
+ }
+ }
+ }
+
+ // TODO: Check IKE over ESP sockets (IPv4, IPv6) - does this need SOCK_RAW?
+
+ /* TODO: Re-enable these when policy matcher works for reflected packets
+ *
+ * The issue here is that A sends to B, and everything is new; therefore PREROUTING counts
+ * correctly. But it appears that the security path is not cleared afterwards, thus when A
+ * sends an ACK back to B, the policy matcher flags it as a "IPSec" packet. See b/70635417
+ */
+
+ // public void testInterfaceCountersTcp4() throws Exception {
+ // IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ // IpSecAlgorithm auth = new IpSecAlgorithm(
+ // IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ // checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, false, 1000);
+ // }
+
+ // public void testInterfaceCountersTcp6() throws Exception {
+ // IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ // IpSecAlgorithm auth = new IpSecAlgorithm(
+ // IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ // checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, false, 1000);
+ // }
+
+ // public void testInterfaceCountersTcp4UdpEncap() throws Exception {
+ // IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ // IpSecAlgorithm auth =
+ // new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ // checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, true, 1000);
+ // }
+
+ @Test
+ public void testInterfaceCountersUdp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1000, false);
+ }
+
+ @Test
+ public void testInterfaceCountersUdp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1000, false);
+ }
+
+ @Test
+ public void testInterfaceCountersUdp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1000, false);
+ }
+
+ @Test
+ public void testAesCbcHmacMd5Tcp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacMd5Tcp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacMd5Udp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacMd5Udp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha1Tcp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha1Tcp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha1Udp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha1Udp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha256Tcp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha256Tcp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha256Udp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha256Udp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha384Tcp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha384Tcp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha384Udp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha384Udp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha512Tcp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha512Tcp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha512Udp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha512Udp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm64Tcp4() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm64Tcp6() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm64Udp4() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm64Udp6() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm96Tcp4() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm96Tcp6() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm96Udp4() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm96Udp6() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm128Tcp4() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm128Tcp6() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm128Udp4() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesGcm128Udp6() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacMd5Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacMd5Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha1Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha1Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha256Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha256Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha384Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha384Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha512Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesCbcHmacSha512Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testAesGcm64Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+ }
+
+ @Test
+ public void testAesGcm64Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+ }
+
+ @Test
+ public void testAesGcm96Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+ }
+
+ @Test
+ public void testAesGcm96Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+ }
+
+ @Test
+ public void testAesGcm128Tcp4UdpEncap() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+ }
+
+ @Test
+ public void testAesGcm128Udp4UdpEncap() throws Exception {
+ IpSecAlgorithm authCrypt =
+ new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+ }
+
+ @Test
+ public void testCryptUdp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, null, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, null, null, false, 1, true);
+ }
+
+ @Test
+ public void testAuthUdp4() throws Exception {
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testCryptUdp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, null, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, null, null, false, 1, true);
+ }
+
+ @Test
+ public void testAuthUdp6() throws Exception {
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, auth, null, false, 1, false);
+ checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testCryptTcp4() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, null, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, null, null, false, 1, true);
+ }
+
+ @Test
+ public void testAuthTcp4() throws Exception {
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testCryptTcp6() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, null, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, null, null, false, 1, true);
+ }
+
+ @Test
+ public void testAuthTcp6() throws Exception {
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, auth, null, false, 1, false);
+ checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, auth, null, false, 1, true);
+ }
+
+ @Test
+ public void testCryptUdp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, null, null, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, null, null, true, 1, true);
+ }
+
+ @Test
+ public void testAuthUdp4UdpEncap() throws Exception {
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, auth, null, true, 1, false);
+ checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testCryptTcp4UdpEncap() throws Exception {
+ IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, null, null, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, null, null, true, 1, true);
+ }
+
+ @Test
+ public void testAuthTcp4UdpEncap() throws Exception {
+ IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, auth, null, true, 1, false);
+ checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, auth, null, true, 1, true);
+ }
+
+ @Test
+ public void testOpenUdpEncapSocketSpecificPort() throws Exception {
+ IpSecManager.UdpEncapsulationSocket encapSocket = null;
+ int port = -1;
+ for (int i = 0; i < MAX_PORT_BIND_ATTEMPTS; i++) {
+ try {
+ port = findUnusedPort();
+ encapSocket = mISM.openUdpEncapsulationSocket(port);
+ break;
+ } catch (ErrnoException e) {
+ if (e.errno == OsConstants.EADDRINUSE) {
+ // Someone claimed the port since we called findUnusedPort.
+ continue;
+ }
+ throw e;
+ } finally {
+ if (encapSocket != null) {
+ encapSocket.close();
+ }
+ }
+ }
+
+ if (encapSocket == null) {
+ fail("Failed " + MAX_PORT_BIND_ATTEMPTS + " attempts to bind to a port");
+ }
+
+ assertTrue("Returned invalid port", encapSocket.getPort() == port);
+ }
+
+ @Test
+ public void testOpenUdpEncapSocketRandomPort() throws Exception {
+ try (IpSecManager.UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket()) {
+ assertTrue("Returned invalid port", encapSocket.getPort() != 0);
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
new file mode 100644
index 0000000..ae38faa
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
@@ -0,0 +1,899 @@
+/*
+ * Copyright (C) 2018 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 static android.app.AppOpsManager.OP_MANAGE_IPSEC_TUNNELS;
+import static android.net.IpSecManager.UdpEncapsulationSocket;
+import static android.net.cts.PacketUtils.AES_CBC_BLK_SIZE;
+import static android.net.cts.PacketUtils.AES_CBC_IV_LEN;
+import static android.net.cts.PacketUtils.BytePayload;
+import static android.net.cts.PacketUtils.EspHeader;
+import static android.net.cts.PacketUtils.IP4_HDRLEN;
+import static android.net.cts.PacketUtils.IP6_HDRLEN;
+import static android.net.cts.PacketUtils.IpHeader;
+import static android.net.cts.PacketUtils.UDP_HDRLEN;
+import static android.net.cts.PacketUtils.UdpHeader;
+import static android.net.cts.PacketUtils.getIpHeader;
+import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.IpSecAlgorithm;
+import android.net.IpSecManager;
+import android.net.IpSecTransform;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.cts.PacketUtils.Payload;
+import android.net.cts.util.CtsNetUtils;
+import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "MANAGE_TEST_NETWORKS permission can't be granted to instant apps")
+public class IpSecManagerTunnelTest extends IpSecBaseTest {
+ private static final String TAG = IpSecManagerTunnelTest.class.getSimpleName();
+
+ private static final InetAddress LOCAL_OUTER_4 = InetAddress.parseNumericAddress("192.0.2.1");
+ private static final InetAddress REMOTE_OUTER_4 = InetAddress.parseNumericAddress("192.0.2.2");
+ private static final InetAddress LOCAL_OUTER_6 =
+ InetAddress.parseNumericAddress("2001:db8:1::1");
+ private static final InetAddress REMOTE_OUTER_6 =
+ InetAddress.parseNumericAddress("2001:db8:1::2");
+
+ private static final InetAddress LOCAL_INNER_4 =
+ InetAddress.parseNumericAddress("198.51.100.1");
+ private static final InetAddress REMOTE_INNER_4 =
+ InetAddress.parseNumericAddress("198.51.100.2");
+ private static final InetAddress LOCAL_INNER_6 =
+ InetAddress.parseNumericAddress("2001:db8:2::1");
+ private static final InetAddress REMOTE_INNER_6 =
+ InetAddress.parseNumericAddress("2001:db8:2::2");
+
+ private static final int IP4_PREFIX_LEN = 32;
+ private static final int IP6_PREFIX_LEN = 128;
+
+ private static final int TIMEOUT_MS = 500;
+
+ // Static state to reduce setup/teardown
+ private static ConnectivityManager sCM;
+ private static TestNetworkManager sTNM;
+ private static ParcelFileDescriptor sTunFd;
+ private static TestNetworkCallback sTunNetworkCallback;
+ private static Network sTunNetwork;
+ private static TunUtils sTunUtils;
+
+ private static Context sContext = InstrumentationRegistry.getContext();
+ private static final CtsNetUtils mCtsNetUtils = new CtsNetUtils(sContext);
+
+ @BeforeClass
+ public static void setUpBeforeClass() throws Exception {
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity();
+ sCM = (ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ sTNM = (TestNetworkManager) sContext.getSystemService(Context.TEST_NETWORK_SERVICE);
+
+ // Under normal circumstances, the MANAGE_IPSEC_TUNNELS appop would be auto-granted, and
+ // a standard permission is insufficient. So we shell out the appop, to give us the
+ // right appop permissions.
+ mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, true);
+
+ TestNetworkInterface testIface =
+ sTNM.createTunInterface(
+ new LinkAddress[] {
+ new LinkAddress(LOCAL_OUTER_4, IP4_PREFIX_LEN),
+ new LinkAddress(LOCAL_OUTER_6, IP6_PREFIX_LEN)
+ });
+
+ sTunFd = testIface.getFileDescriptor();
+ sTunNetworkCallback = mCtsNetUtils.setupAndGetTestNetwork(testIface.getInterfaceName());
+ sTunNetworkCallback.waitForAvailable();
+ sTunNetwork = sTunNetworkCallback.currentNetwork;
+
+ sTunUtils = new TunUtils(sTunFd);
+ }
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ // Set to true before every run; some tests flip this.
+ mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, true);
+
+ // Clear sTunUtils state
+ sTunUtils.reset();
+ }
+
+ @AfterClass
+ public static void tearDownAfterClass() throws Exception {
+ mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, false);
+
+ sCM.unregisterNetworkCallback(sTunNetworkCallback);
+
+ sTNM.teardownTestNetwork(sTunNetwork);
+ sTunFd.close();
+
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+
+ @Test
+ public void testSecurityExceptionCreateTunnelInterfaceWithoutAppop() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ // Ensure we don't have the appop. Permission is not requested in the Manifest
+ mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, false);
+
+ // Security exceptions are thrown regardless of IPv4/IPv6. Just test one
+ try {
+ mISM.createIpSecTunnelInterface(LOCAL_INNER_6, REMOTE_INNER_6, sTunNetwork);
+ fail("Did not throw SecurityException for Tunnel creation without appop");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ @Test
+ public void testSecurityExceptionBuildTunnelTransformWithoutAppop() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+
+ // Ensure we don't have the appop. Permission is not requested in the Manifest
+ mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, false);
+
+ // Security exceptions are thrown regardless of IPv4/IPv6. Just test one
+ try (IpSecManager.SecurityParameterIndex spi =
+ mISM.allocateSecurityParameterIndex(LOCAL_INNER_4);
+ IpSecTransform transform =
+ new IpSecTransform.Builder(sContext)
+ .buildTunnelModeTransform(REMOTE_INNER_4, spi)) {
+ fail("Did not throw SecurityException for Transform creation without appop");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ /* Test runnables for callbacks after IPsec tunnels are set up. */
+ private abstract class IpSecTunnelTestRunnable {
+ /**
+ * Runs the test code, and returns the inner socket port, if any.
+ *
+ * @param ipsecNetwork The IPsec Interface based Network for binding sockets on
+ * @return the integer port of the inner socket if outbound, or 0 if inbound
+ * IpSecTunnelTestRunnable
+ * @throws Exception if any part of the test failed.
+ */
+ public abstract int run(Network ipsecNetwork) throws Exception;
+ }
+
+ private int getPacketSize(
+ int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode) {
+ int expectedPacketSize = TEST_DATA.length + UDP_HDRLEN;
+
+ // Inner Transport mode packet size
+ if (transportInTunnelMode) {
+ expectedPacketSize =
+ PacketUtils.calculateEspPacketSize(
+ expectedPacketSize,
+ AES_CBC_IV_LEN,
+ AES_CBC_BLK_SIZE,
+ AUTH_KEY.length * 4);
+ }
+
+ // Inner IP Header
+ expectedPacketSize += innerFamily == AF_INET ? IP4_HDRLEN : IP6_HDRLEN;
+
+ // Tunnel mode transform size
+ expectedPacketSize =
+ PacketUtils.calculateEspPacketSize(
+ expectedPacketSize, AES_CBC_IV_LEN, AES_CBC_BLK_SIZE, AUTH_KEY.length * 4);
+
+ // UDP encap size
+ expectedPacketSize += useEncap ? UDP_HDRLEN : 0;
+
+ // Outer IP Header
+ expectedPacketSize += outerFamily == AF_INET ? IP4_HDRLEN : IP6_HDRLEN;
+
+ return expectedPacketSize;
+ }
+
+ private interface IpSecTunnelTestRunnableFactory {
+ IpSecTunnelTestRunnable getIpSecTunnelTestRunnable(
+ boolean transportInTunnelMode,
+ int spi,
+ InetAddress localInner,
+ InetAddress remoteInner,
+ InetAddress localOuter,
+ InetAddress remoteOuter,
+ IpSecTransform inTransportTransform,
+ IpSecTransform outTransportTransform,
+ int encapPort,
+ int innerSocketPort,
+ int expectedPacketSize)
+ throws Exception;
+ }
+
+ private class OutputIpSecTunnelTestRunnableFactory implements IpSecTunnelTestRunnableFactory {
+ public IpSecTunnelTestRunnable getIpSecTunnelTestRunnable(
+ boolean transportInTunnelMode,
+ int spi,
+ InetAddress localInner,
+ InetAddress remoteInner,
+ InetAddress localOuter,
+ InetAddress remoteOuter,
+ IpSecTransform inTransportTransform,
+ IpSecTransform outTransportTransform,
+ int encapPort,
+ int unusedInnerSocketPort,
+ int expectedPacketSize) {
+ return new IpSecTunnelTestRunnable() {
+ @Override
+ public int run(Network ipsecNetwork) throws Exception {
+ // Build a socket and send traffic
+ JavaUdpSocket socket = new JavaUdpSocket(localInner);
+ ipsecNetwork.bindSocket(socket.mSocket);
+ int innerSocketPort = socket.getPort();
+
+ // For Transport-In-Tunnel mode, apply transform to socket
+ if (transportInTunnelMode) {
+ mISM.applyTransportModeTransform(
+ socket.mSocket, IpSecManager.DIRECTION_IN, inTransportTransform);
+ mISM.applyTransportModeTransform(
+ socket.mSocket, IpSecManager.DIRECTION_OUT, outTransportTransform);
+ }
+
+ socket.sendTo(TEST_DATA, remoteInner, socket.getPort());
+
+ // Verify that an encrypted packet is sent. As of right now, checking encrypted
+ // body is not possible, due to the test not knowing some of the fields of the
+ // inner IP header (flow label, flags, etc)
+ sTunUtils.awaitEspPacketNoPlaintext(
+ spi, TEST_DATA, encapPort != 0, expectedPacketSize);
+
+ socket.close();
+
+ return innerSocketPort;
+ }
+ };
+ }
+ }
+
+ private class InputReflectedIpSecTunnelTestRunnableFactory
+ implements IpSecTunnelTestRunnableFactory {
+ public IpSecTunnelTestRunnable getIpSecTunnelTestRunnable(
+ boolean transportInTunnelMode,
+ int spi,
+ InetAddress localInner,
+ InetAddress remoteInner,
+ InetAddress localOuter,
+ InetAddress remoteOuter,
+ IpSecTransform inTransportTransform,
+ IpSecTransform outTransportTransform,
+ int encapPort,
+ int innerSocketPort,
+ int expectedPacketSize)
+ throws Exception {
+ return new IpSecTunnelTestRunnable() {
+ @Override
+ public int run(Network ipsecNetwork) throws Exception {
+ // Build a socket and receive traffic
+ JavaUdpSocket socket = new JavaUdpSocket(localInner, innerSocketPort);
+ ipsecNetwork.bindSocket(socket.mSocket);
+
+ // For Transport-In-Tunnel mode, apply transform to socket
+ if (transportInTunnelMode) {
+ mISM.applyTransportModeTransform(
+ socket.mSocket, IpSecManager.DIRECTION_IN, outTransportTransform);
+ mISM.applyTransportModeTransform(
+ socket.mSocket, IpSecManager.DIRECTION_OUT, inTransportTransform);
+ }
+
+ sTunUtils.reflectPackets();
+
+ // Receive packet from socket, and validate that the payload is correct
+ receiveAndValidatePacket(socket);
+
+ socket.close();
+
+ return 0;
+ }
+ };
+ }
+ }
+
+ private class InputPacketGeneratorIpSecTunnelTestRunnableFactory
+ implements IpSecTunnelTestRunnableFactory {
+ public IpSecTunnelTestRunnable getIpSecTunnelTestRunnable(
+ boolean transportInTunnelMode,
+ int spi,
+ InetAddress localInner,
+ InetAddress remoteInner,
+ InetAddress localOuter,
+ InetAddress remoteOuter,
+ IpSecTransform inTransportTransform,
+ IpSecTransform outTransportTransform,
+ int encapPort,
+ int innerSocketPort,
+ int expectedPacketSize)
+ throws Exception {
+ return new IpSecTunnelTestRunnable() {
+ @Override
+ public int run(Network ipsecNetwork) throws Exception {
+ // Build a socket and receive traffic
+ JavaUdpSocket socket = new JavaUdpSocket(localInner);
+ ipsecNetwork.bindSocket(socket.mSocket);
+
+ // For Transport-In-Tunnel mode, apply transform to socket
+ if (transportInTunnelMode) {
+ mISM.applyTransportModeTransform(
+ socket.mSocket, IpSecManager.DIRECTION_IN, outTransportTransform);
+ mISM.applyTransportModeTransform(
+ socket.mSocket, IpSecManager.DIRECTION_OUT, inTransportTransform);
+ }
+
+ byte[] pkt;
+ if (transportInTunnelMode) {
+ pkt =
+ getTransportInTunnelModePacket(
+ spi,
+ spi,
+ remoteInner,
+ localInner,
+ remoteOuter,
+ localOuter,
+ socket.getPort(),
+ encapPort);
+ } else {
+ pkt =
+ getTunnelModePacket(
+ spi,
+ remoteInner,
+ localInner,
+ remoteOuter,
+ localOuter,
+ socket.getPort(),
+ encapPort);
+ }
+ sTunUtils.injectPacket(pkt);
+
+ // Receive packet from socket, and validate
+ receiveAndValidatePacket(socket);
+
+ socket.close();
+
+ return 0;
+ }
+ };
+ }
+ }
+
+ private void checkTunnelOutput(
+ int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode)
+ throws Exception {
+ checkTunnel(
+ innerFamily,
+ outerFamily,
+ useEncap,
+ transportInTunnelMode,
+ new OutputIpSecTunnelTestRunnableFactory());
+ }
+
+ private void checkTunnelInput(
+ int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode)
+ throws Exception {
+ checkTunnel(
+ innerFamily,
+ outerFamily,
+ useEncap,
+ transportInTunnelMode,
+ new InputPacketGeneratorIpSecTunnelTestRunnableFactory());
+ }
+
+ /**
+ * Validates that the kernel can talk to itself.
+ *
+ * <p>This test takes an outbound IPsec packet, reflects it (by flipping IP src/dst), and
+ * injects it back into the TUN. This test then verifies that a packet with the correct payload
+ * is found on the specified socket/port.
+ */
+ public void checkTunnelReflected(
+ int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode)
+ throws Exception {
+ InetAddress localInner = innerFamily == AF_INET ? LOCAL_INNER_4 : LOCAL_INNER_6;
+ InetAddress remoteInner = innerFamily == AF_INET ? REMOTE_INNER_4 : REMOTE_INNER_6;
+
+ InetAddress localOuter = outerFamily == AF_INET ? LOCAL_OUTER_4 : LOCAL_OUTER_6;
+ InetAddress remoteOuter = outerFamily == AF_INET ? REMOTE_OUTER_4 : REMOTE_OUTER_6;
+
+ // Preselect both SPI and encap port, to be used for both inbound and outbound tunnels.
+ int spi = getRandomSpi(localOuter, remoteOuter);
+ int expectedPacketSize =
+ getPacketSize(innerFamily, outerFamily, useEncap, transportInTunnelMode);
+
+ try (IpSecManager.SecurityParameterIndex inTransportSpi =
+ mISM.allocateSecurityParameterIndex(localInner, spi);
+ IpSecManager.SecurityParameterIndex outTransportSpi =
+ mISM.allocateSecurityParameterIndex(remoteInner, spi);
+ IpSecTransform inTransportTransform =
+ buildIpSecTransform(sContext, inTransportSpi, null, remoteInner);
+ IpSecTransform outTransportTransform =
+ buildIpSecTransform(sContext, outTransportSpi, null, localInner);
+ UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket()) {
+
+ // Run output direction tests
+ IpSecTunnelTestRunnable outputIpSecTunnelTestRunnable =
+ new OutputIpSecTunnelTestRunnableFactory()
+ .getIpSecTunnelTestRunnable(
+ transportInTunnelMode,
+ spi,
+ localInner,
+ remoteInner,
+ localOuter,
+ remoteOuter,
+ inTransportTransform,
+ outTransportTransform,
+ useEncap ? encapSocket.getPort() : 0,
+ 0,
+ expectedPacketSize);
+ int innerSocketPort =
+ buildTunnelNetworkAndRunTests(
+ localInner,
+ remoteInner,
+ localOuter,
+ remoteOuter,
+ spi,
+ useEncap ? encapSocket : null,
+ outputIpSecTunnelTestRunnable);
+
+ // Input direction tests, with matching inner socket ports.
+ IpSecTunnelTestRunnable inputIpSecTunnelTestRunnable =
+ new InputReflectedIpSecTunnelTestRunnableFactory()
+ .getIpSecTunnelTestRunnable(
+ transportInTunnelMode,
+ spi,
+ remoteInner,
+ localInner,
+ localOuter,
+ remoteOuter,
+ inTransportTransform,
+ outTransportTransform,
+ useEncap ? encapSocket.getPort() : 0,
+ innerSocketPort,
+ expectedPacketSize);
+ buildTunnelNetworkAndRunTests(
+ remoteInner,
+ localInner,
+ localOuter,
+ remoteOuter,
+ spi,
+ useEncap ? encapSocket : null,
+ inputIpSecTunnelTestRunnable);
+ }
+ }
+
+ public void checkTunnel(
+ int innerFamily,
+ int outerFamily,
+ boolean useEncap,
+ boolean transportInTunnelMode,
+ IpSecTunnelTestRunnableFactory factory)
+ throws Exception {
+
+ InetAddress localInner = innerFamily == AF_INET ? LOCAL_INNER_4 : LOCAL_INNER_6;
+ InetAddress remoteInner = innerFamily == AF_INET ? REMOTE_INNER_4 : REMOTE_INNER_6;
+
+ InetAddress localOuter = outerFamily == AF_INET ? LOCAL_OUTER_4 : LOCAL_OUTER_6;
+ InetAddress remoteOuter = outerFamily == AF_INET ? REMOTE_OUTER_4 : REMOTE_OUTER_6;
+
+ // Preselect both SPI and encap port, to be used for both inbound and outbound tunnels.
+ // Re-uses the same SPI to ensure that even in cases of symmetric SPIs shared across tunnel
+ // and transport mode, packets are encrypted/decrypted properly based on the src/dst.
+ int spi = getRandomSpi(localOuter, remoteOuter);
+ int expectedPacketSize =
+ getPacketSize(innerFamily, outerFamily, useEncap, transportInTunnelMode);
+
+ try (IpSecManager.SecurityParameterIndex inTransportSpi =
+ mISM.allocateSecurityParameterIndex(localInner, spi);
+ IpSecManager.SecurityParameterIndex outTransportSpi =
+ mISM.allocateSecurityParameterIndex(remoteInner, spi);
+ IpSecTransform inTransportTransform =
+ buildIpSecTransform(sContext, inTransportSpi, null, remoteInner);
+ IpSecTransform outTransportTransform =
+ buildIpSecTransform(sContext, outTransportSpi, null, localInner);
+ UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket()) {
+
+ buildTunnelNetworkAndRunTests(
+ localInner,
+ remoteInner,
+ localOuter,
+ remoteOuter,
+ spi,
+ useEncap ? encapSocket : null,
+ factory.getIpSecTunnelTestRunnable(
+ transportInTunnelMode,
+ spi,
+ localInner,
+ remoteInner,
+ localOuter,
+ remoteOuter,
+ inTransportTransform,
+ outTransportTransform,
+ useEncap ? encapSocket.getPort() : 0,
+ 0,
+ expectedPacketSize));
+ }
+ }
+
+ private int buildTunnelNetworkAndRunTests(
+ InetAddress localInner,
+ InetAddress remoteInner,
+ InetAddress localOuter,
+ InetAddress remoteOuter,
+ int spi,
+ UdpEncapsulationSocket encapSocket,
+ IpSecTunnelTestRunnable test)
+ throws Exception {
+ int innerPrefixLen = localInner instanceof Inet6Address ? IP6_PREFIX_LEN : IP4_PREFIX_LEN;
+ TestNetworkCallback testNetworkCb = null;
+ int innerSocketPort;
+
+ try (IpSecManager.SecurityParameterIndex inSpi =
+ mISM.allocateSecurityParameterIndex(localOuter, spi);
+ IpSecManager.SecurityParameterIndex outSpi =
+ mISM.allocateSecurityParameterIndex(remoteOuter, spi);
+ IpSecManager.IpSecTunnelInterface tunnelIface =
+ mISM.createIpSecTunnelInterface(localOuter, remoteOuter, sTunNetwork)) {
+ // Build the test network
+ tunnelIface.addAddress(localInner, innerPrefixLen);
+ testNetworkCb = mCtsNetUtils.setupAndGetTestNetwork(tunnelIface.getInterfaceName());
+ testNetworkCb.waitForAvailable();
+ Network testNetwork = testNetworkCb.currentNetwork;
+
+ // Check interface was created
+ assertNotNull(NetworkInterface.getByName(tunnelIface.getInterfaceName()));
+
+ // Verify address was added
+ final NetworkInterface netIface = NetworkInterface.getByInetAddress(localInner);
+ assertNotNull(netIface);
+ assertEquals(tunnelIface.getInterfaceName(), netIface.getDisplayName());
+
+ // Configure Transform parameters
+ IpSecTransform.Builder transformBuilder = new IpSecTransform.Builder(sContext);
+ transformBuilder.setEncryption(
+ new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY));
+ transformBuilder.setAuthentication(
+ new IpSecAlgorithm(
+ IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4));
+
+ if (encapSocket != null) {
+ transformBuilder.setIpv4Encapsulation(encapSocket, encapSocket.getPort());
+ }
+
+ // Apply transform and check that traffic is properly encrypted
+ try (IpSecTransform inTransform =
+ transformBuilder.buildTunnelModeTransform(remoteOuter, inSpi);
+ IpSecTransform outTransform =
+ transformBuilder.buildTunnelModeTransform(localOuter, outSpi)) {
+ mISM.applyTunnelModeTransform(tunnelIface, IpSecManager.DIRECTION_IN, inTransform);
+ mISM.applyTunnelModeTransform(
+ tunnelIface, IpSecManager.DIRECTION_OUT, outTransform);
+
+ innerSocketPort = test.run(testNetwork);
+ }
+
+ // Teardown the test network
+ sTNM.teardownTestNetwork(testNetwork);
+
+ // Remove addresses and check that interface is still present, but fails lookup-by-addr
+ tunnelIface.removeAddress(localInner, innerPrefixLen);
+ assertNotNull(NetworkInterface.getByName(tunnelIface.getInterfaceName()));
+ assertNull(NetworkInterface.getByInetAddress(localInner));
+
+ // Check interface was cleaned up
+ tunnelIface.close();
+ assertNull(NetworkInterface.getByName(tunnelIface.getInterfaceName()));
+ } finally {
+ if (testNetworkCb != null) {
+ sCM.unregisterNetworkCallback(testNetworkCb);
+ }
+ }
+
+ return innerSocketPort;
+ }
+
+ private static void receiveAndValidatePacket(JavaUdpSocket socket) throws Exception {
+ byte[] socketResponseBytes = socket.receive();
+ assertArrayEquals(TEST_DATA, socketResponseBytes);
+ }
+
+ private int getRandomSpi(InetAddress localOuter, InetAddress remoteOuter) throws Exception {
+ // Try to allocate both in and out SPIs using the same requested SPI value.
+ try (IpSecManager.SecurityParameterIndex inSpi =
+ mISM.allocateSecurityParameterIndex(localOuter);
+ IpSecManager.SecurityParameterIndex outSpi =
+ mISM.allocateSecurityParameterIndex(remoteOuter, inSpi.getSpi()); ) {
+ return inSpi.getSpi();
+ }
+ }
+
+ private EspHeader buildTransportModeEspPacket(
+ int spi, InetAddress src, InetAddress dst, int port, Payload payload) throws Exception {
+ IpHeader preEspIpHeader = getIpHeader(payload.getProtocolId(), src, dst, payload);
+
+ return new EspHeader(
+ payload.getProtocolId(),
+ spi,
+ 1, // sequence number
+ CRYPT_KEY, // Same key for auth and crypt
+ payload.getPacketBytes(preEspIpHeader));
+ }
+
+ private EspHeader buildTunnelModeEspPacket(
+ int spi,
+ InetAddress srcInner,
+ InetAddress dstInner,
+ InetAddress srcOuter,
+ InetAddress dstOuter,
+ int port,
+ int encapPort,
+ Payload payload)
+ throws Exception {
+ IpHeader innerIp = getIpHeader(payload.getProtocolId(), srcInner, dstInner, payload);
+ return new EspHeader(
+ innerIp.getProtocolId(),
+ spi,
+ 1, // sequence number
+ CRYPT_KEY, // Same key for auth and crypt
+ innerIp.getPacketBytes());
+ }
+
+ private IpHeader maybeEncapPacket(
+ InetAddress src, InetAddress dst, int encapPort, EspHeader espPayload)
+ throws Exception {
+
+ Payload payload = espPayload;
+ if (encapPort != 0) {
+ payload = new UdpHeader(encapPort, encapPort, espPayload);
+ }
+
+ return getIpHeader(payload.getProtocolId(), src, dst, payload);
+ }
+
+ private byte[] getTunnelModePacket(
+ int spi,
+ InetAddress srcInner,
+ InetAddress dstInner,
+ InetAddress srcOuter,
+ InetAddress dstOuter,
+ int port,
+ int encapPort)
+ throws Exception {
+ UdpHeader udp = new UdpHeader(port, port, new BytePayload(TEST_DATA));
+
+ EspHeader espPayload =
+ buildTunnelModeEspPacket(
+ spi, srcInner, dstInner, srcOuter, dstOuter, port, encapPort, udp);
+ return maybeEncapPacket(srcOuter, dstOuter, encapPort, espPayload).getPacketBytes();
+ }
+
+ private byte[] getTransportInTunnelModePacket(
+ int spiInner,
+ int spiOuter,
+ InetAddress srcInner,
+ InetAddress dstInner,
+ InetAddress srcOuter,
+ InetAddress dstOuter,
+ int port,
+ int encapPort)
+ throws Exception {
+ UdpHeader udp = new UdpHeader(port, port, new BytePayload(TEST_DATA));
+
+ EspHeader espPayload = buildTransportModeEspPacket(spiInner, srcInner, dstInner, port, udp);
+ espPayload =
+ buildTunnelModeEspPacket(
+ spiOuter,
+ srcInner,
+ dstInner,
+ srcOuter,
+ dstOuter,
+ port,
+ encapPort,
+ espPayload);
+ return maybeEncapPacket(srcOuter, dstOuter, encapPort, espPayload).getPacketBytes();
+ }
+
+ // Transport-in-Tunnel mode tests
+ @Test
+ public void testTransportInTunnelModeV4InV4() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET, AF_INET, false, true);
+ checkTunnelInput(AF_INET, AF_INET, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV4InV4Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV4InV4UdpEncap() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET, AF_INET, true, true);
+ checkTunnelInput(AF_INET, AF_INET, true, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV4InV4UdpEncapReflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV4InV6() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET, AF_INET6, false, true);
+ checkTunnelInput(AF_INET, AF_INET6, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV4InV6Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV6InV4() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET6, AF_INET, false, true);
+ checkTunnelInput(AF_INET6, AF_INET, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV6InV4Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV6InV4UdpEncap() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET6, AF_INET, true, true);
+ checkTunnelInput(AF_INET6, AF_INET, true, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV6InV4UdpEncapReflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV6InV6() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET, AF_INET6, false, true);
+ checkTunnelInput(AF_INET, AF_INET6, false, true);
+ }
+
+ @Test
+ public void testTransportInTunnelModeV6InV6Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, false, true);
+ }
+
+ // Tunnel mode tests
+ @Test
+ public void testTunnelV4InV4() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET, AF_INET, false, false);
+ checkTunnelInput(AF_INET, AF_INET, false, false);
+ }
+
+ @Test
+ public void testTunnelV4InV4Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, false, false);
+ }
+
+ @Test
+ public void testTunnelV4InV4UdpEncap() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET, AF_INET, true, false);
+ checkTunnelInput(AF_INET, AF_INET, true, false);
+ }
+
+ @Test
+ public void testTunnelV4InV4UdpEncapReflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET, true, false);
+ }
+
+ @Test
+ public void testTunnelV4InV6() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET, AF_INET6, false, false);
+ checkTunnelInput(AF_INET, AF_INET6, false, false);
+ }
+
+ @Test
+ public void testTunnelV4InV6Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET, AF_INET6, false, false);
+ }
+
+ @Test
+ public void testTunnelV6InV4() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET6, AF_INET, false, false);
+ checkTunnelInput(AF_INET6, AF_INET, false, false);
+ }
+
+ @Test
+ public void testTunnelV6InV4Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET6, AF_INET, false, false);
+ }
+
+ @Test
+ public void testTunnelV6InV4UdpEncap() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET6, AF_INET, true, false);
+ checkTunnelInput(AF_INET6, AF_INET, true, false);
+ }
+
+ @Test
+ public void testTunnelV6InV4UdpEncapReflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET6, AF_INET, true, false);
+ }
+
+ @Test
+ public void testTunnelV6InV6() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelOutput(AF_INET6, AF_INET6, false, false);
+ checkTunnelInput(AF_INET6, AF_INET6, false, false);
+ }
+
+ @Test
+ public void testTunnelV6InV6Reflected() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ checkTunnelReflected(AF_INET6, AF_INET6, false, false);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/LocalServerSocketTest.java b/tests/cts/net/src/android/net/cts/LocalServerSocketTest.java
new file mode 100644
index 0000000..7c5a1b3
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/LocalServerSocketTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2009 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 junit.framework.TestCase;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import android.net.LocalSocketAddress;
+
+public class LocalServerSocketTest extends TestCase {
+
+ public void testLocalServerSocket() throws IOException {
+ String address = "com.android.net.LocalServerSocketTest_testLocalServerSocket";
+ LocalServerSocket localServerSocket = new LocalServerSocket(address);
+ assertNotNull(localServerSocket.getLocalSocketAddress());
+
+ // create client socket
+ LocalSocket clientSocket = new LocalSocket();
+
+ // establish connection between client and server
+ clientSocket.connect(new LocalSocketAddress(address));
+ LocalSocket serverSocket = localServerSocket.accept();
+
+ assertTrue(serverSocket.isConnected());
+ assertTrue(serverSocket.isBound());
+
+ // send data from client to server
+ OutputStream clientOutStream = clientSocket.getOutputStream();
+ clientOutStream.write(12);
+ InputStream serverInStream = serverSocket.getInputStream();
+ assertEquals(12, serverInStream.read());
+
+ // send data from server to client
+ OutputStream serverOutStream = serverSocket.getOutputStream();
+ serverOutStream.write(3);
+ InputStream clientInStream = clientSocket.getInputStream();
+ assertEquals(3, clientInStream.read());
+
+ // close server socket
+ assertNotNull(localServerSocket.getFileDescriptor());
+ localServerSocket.close();
+ assertNull(localServerSocket.getFileDescriptor());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/LocalSocketAddressTest.java b/tests/cts/net/src/android/net/cts/LocalSocketAddressTest.java
new file mode 100644
index 0000000..6ef003b
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/LocalSocketAddressTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2008 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.LocalSocketAddress;
+import android.net.LocalSocketAddress.Namespace;
+import android.test.AndroidTestCase;
+
+public class LocalSocketAddressTest extends AndroidTestCase {
+
+ public void testNewLocalSocketAddressWithDefaultNamespace() {
+ // default namespace
+ LocalSocketAddress localSocketAddress = new LocalSocketAddress("name");
+ assertEquals("name", localSocketAddress.getName());
+ assertEquals(Namespace.ABSTRACT, localSocketAddress.getNamespace());
+
+ // specify the namespace
+ LocalSocketAddress localSocketAddress2 =
+ new LocalSocketAddress("name2", Namespace.ABSTRACT);
+ assertEquals("name2", localSocketAddress2.getName());
+ assertEquals(Namespace.ABSTRACT, localSocketAddress2.getNamespace());
+
+ LocalSocketAddress localSocketAddress3 =
+ new LocalSocketAddress("name3", Namespace.FILESYSTEM);
+ assertEquals("name3", localSocketAddress3.getName());
+ assertEquals(Namespace.FILESYSTEM, localSocketAddress3.getNamespace());
+
+ LocalSocketAddress localSocketAddress4 =
+ new LocalSocketAddress("name4", Namespace.RESERVED);
+ assertEquals("name4", localSocketAddress4.getName());
+ assertEquals(Namespace.RESERVED, localSocketAddress4.getNamespace());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/LocalSocketAddress_NamespaceTest.java b/tests/cts/net/src/android/net/cts/LocalSocketAddress_NamespaceTest.java
new file mode 100644
index 0000000..97dfa43
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/LocalSocketAddress_NamespaceTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2009 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.LocalSocketAddress.Namespace;
+import android.test.AndroidTestCase;
+
+public class LocalSocketAddress_NamespaceTest extends AndroidTestCase {
+
+ public void testValueOf() {
+ assertEquals(Namespace.ABSTRACT, Namespace.valueOf("ABSTRACT"));
+ assertEquals(Namespace.RESERVED, Namespace.valueOf("RESERVED"));
+ assertEquals(Namespace.FILESYSTEM, Namespace.valueOf("FILESYSTEM"));
+ }
+
+ public void testValues() {
+ Namespace[] expected = Namespace.values();
+ assertEquals(Namespace.ABSTRACT, expected[0]);
+ assertEquals(Namespace.RESERVED, expected[1]);
+ assertEquals(Namespace.FILESYSTEM, expected[2]);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/LocalSocketTest.java b/tests/cts/net/src/android/net/cts/LocalSocketTest.java
new file mode 100644
index 0000000..6e61705
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/LocalSocketTest.java
@@ -0,0 +1,470 @@
+/*
+ * Copyright (C) 2008 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 junit.framework.TestCase;
+
+import android.net.Credentials;
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import android.net.LocalSocketAddress;
+import android.system.Os;
+import android.system.OsConstants;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+public class LocalSocketTest extends TestCase {
+ private final static String ADDRESS_PREFIX = "com.android.net.LocalSocketTest";
+
+ public void testLocalConnections() throws IOException {
+ String address = ADDRESS_PREFIX + "_testLocalConnections";
+ // create client and server socket
+ LocalServerSocket localServerSocket = new LocalServerSocket(address);
+ LocalSocket clientSocket = new LocalSocket();
+
+ // establish connection between client and server
+ LocalSocketAddress locSockAddr = new LocalSocketAddress(address);
+ assertFalse(clientSocket.isConnected());
+ clientSocket.connect(locSockAddr);
+ assertTrue(clientSocket.isConnected());
+
+ LocalSocket serverSocket = localServerSocket.accept();
+ assertTrue(serverSocket.isConnected());
+ assertTrue(serverSocket.isBound());
+ try {
+ serverSocket.bind(localServerSocket.getLocalSocketAddress());
+ fail("Cannot bind a LocalSocket from accept()");
+ } catch (IOException expected) {
+ }
+ try {
+ serverSocket.connect(locSockAddr);
+ fail("Cannot connect a LocalSocket from accept()");
+ } catch (IOException expected) {
+ }
+
+ Credentials credent = clientSocket.getPeerCredentials();
+ assertTrue(0 != credent.getPid());
+
+ // send data from client to server
+ OutputStream clientOutStream = clientSocket.getOutputStream();
+ clientOutStream.write(12);
+ InputStream serverInStream = serverSocket.getInputStream();
+ assertEquals(12, serverInStream.read());
+
+ //send data from server to client
+ OutputStream serverOutStream = serverSocket.getOutputStream();
+ serverOutStream.write(3);
+ InputStream clientInStream = clientSocket.getInputStream();
+ assertEquals(3, clientInStream.read());
+
+ // Test sending and receiving file descriptors
+ clientSocket.setFileDescriptorsForSend(new FileDescriptor[]{FileDescriptor.in});
+ clientOutStream.write(32);
+ assertEquals(32, serverInStream.read());
+
+ FileDescriptor[] out = serverSocket.getAncillaryFileDescriptors();
+ assertEquals(1, out.length);
+ FileDescriptor fd = clientSocket.getFileDescriptor();
+ assertTrue(fd.valid());
+
+ //shutdown input stream of client
+ clientSocket.shutdownInput();
+ assertEquals(-1, clientInStream.read());
+
+ //shutdown output stream of client
+ clientSocket.shutdownOutput();
+ try {
+ clientOutStream.write(10);
+ fail("testLocalSocket shouldn't come to here");
+ } catch (IOException e) {
+ // expected
+ }
+
+ //shutdown input stream of server
+ serverSocket.shutdownInput();
+ assertEquals(-1, serverInStream.read());
+
+ //shutdown output stream of server
+ serverSocket.shutdownOutput();
+ try {
+ serverOutStream.write(10);
+ fail("testLocalSocket shouldn't come to here");
+ } catch (IOException e) {
+ // expected
+ }
+
+ //close client socket
+ clientSocket.close();
+ try {
+ clientInStream.read();
+ fail("testLocalSocket shouldn't come to here");
+ } catch (IOException e) {
+ // expected
+ }
+
+ //close server socket
+ serverSocket.close();
+ try {
+ serverInStream.read();
+ fail("testLocalSocket shouldn't come to here");
+ } catch (IOException e) {
+ // expected
+ }
+ }
+
+ public void testAccessors() throws IOException {
+ String address = ADDRESS_PREFIX + "_testAccessors";
+ LocalSocket socket = new LocalSocket();
+ LocalSocketAddress addr = new LocalSocketAddress(address);
+
+ assertFalse(socket.isBound());
+ socket.bind(addr);
+ assertTrue(socket.isBound());
+ assertEquals(addr, socket.getLocalSocketAddress());
+
+ String str = socket.toString();
+ assertTrue(str.contains("impl:android.net.LocalSocketImpl"));
+
+ socket.setReceiveBufferSize(1999);
+ assertEquals(1999 << 1, socket.getReceiveBufferSize());
+
+ socket.setSendBufferSize(3998);
+ assertEquals(3998 << 1, socket.getSendBufferSize());
+
+ assertEquals(0, socket.getSoTimeout());
+ socket.setSoTimeout(1996);
+ assertTrue(socket.getSoTimeout() > 0);
+
+ try {
+ socket.getRemoteSocketAddress();
+ fail("testLocalSocketSecondary shouldn't come to here");
+ } catch (UnsupportedOperationException e) {
+ // expected
+ }
+
+ try {
+ socket.isClosed();
+ fail("testLocalSocketSecondary shouldn't come to here");
+ } catch (UnsupportedOperationException e) {
+ // expected
+ }
+
+ try {
+ socket.isInputShutdown();
+ fail("testLocalSocketSecondary shouldn't come to here");
+ } catch (UnsupportedOperationException e) {
+ // expected
+ }
+
+ try {
+ socket.isOutputShutdown();
+ fail("testLocalSocketSecondary shouldn't come to here");
+ } catch (UnsupportedOperationException e) {
+ // expected
+ }
+
+ try {
+ socket.connect(addr, 2005);
+ fail("testLocalSocketSecondary shouldn't come to here");
+ } catch (UnsupportedOperationException e) {
+ // expected
+ }
+
+ socket.close();
+ }
+
+ // http://b/31205169
+ public void testSetSoTimeout_readTimeout() throws Exception {
+ String address = ADDRESS_PREFIX + "_testSetSoTimeout_readTimeout";
+
+ try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
+ final LocalSocket clientSocket = socketPair.clientSocket;
+
+ // Set the timeout in millis.
+ int timeoutMillis = 1000;
+ clientSocket.setSoTimeout(timeoutMillis);
+
+ // Avoid blocking the test run if timeout doesn't happen by using a separate thread.
+ Callable<Result> reader = () -> {
+ try {
+ clientSocket.getInputStream().read();
+ return Result.noException("Did not block");
+ } catch (IOException e) {
+ return Result.exception(e);
+ }
+ };
+ // Allow the configured timeout, plus some slop.
+ int allowedTime = timeoutMillis + 2000;
+ Result result = runInSeparateThread(allowedTime, reader);
+
+ // Check the message was a timeout, it's all we have to go on.
+ String expectedMessage = Os.strerror(OsConstants.EAGAIN);
+ result.assertThrewIOException(expectedMessage);
+ }
+ }
+
+ // http://b/31205169
+ public void testSetSoTimeout_writeTimeout() throws Exception {
+ String address = ADDRESS_PREFIX + "_testSetSoTimeout_writeTimeout";
+
+ try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
+ final LocalSocket clientSocket = socketPair.clientSocket;
+
+ // Set the timeout in millis.
+ int timeoutMillis = 1000;
+ clientSocket.setSoTimeout(timeoutMillis);
+
+ // Set a small buffer size so we know we can flood it.
+ clientSocket.setSendBufferSize(100);
+ final int bufferSize = clientSocket.getSendBufferSize();
+
+ // Avoid blocking the test run if timeout doesn't happen by using a separate thread.
+ Callable<Result> writer = () -> {
+ try {
+ byte[] toWrite = new byte[bufferSize * 2];
+ clientSocket.getOutputStream().write(toWrite);
+ return Result.noException("Did not block");
+ } catch (IOException e) {
+ return Result.exception(e);
+ }
+ };
+ // Allow the configured timeout, plus some slop.
+ int allowedTime = timeoutMillis + 2000;
+
+ Result result = runInSeparateThread(allowedTime, writer);
+
+ // Check the message was a timeout, it's all we have to go on.
+ String expectedMessage = Os.strerror(OsConstants.EAGAIN);
+ result.assertThrewIOException(expectedMessage);
+ }
+ }
+
+ public void testAvailable() throws Exception {
+ String address = ADDRESS_PREFIX + "_testAvailable";
+
+ try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
+ LocalSocket clientSocket = socketPair.clientSocket;
+ LocalSocket serverSocket = socketPair.serverSocket.accept();
+
+ OutputStream clientOutputStream = clientSocket.getOutputStream();
+ InputStream serverInputStream = serverSocket.getInputStream();
+ assertEquals(0, serverInputStream.available());
+
+ byte[] buffer = new byte[50];
+ clientOutputStream.write(buffer);
+ assertEquals(50, serverInputStream.available());
+
+ InputStream clientInputStream = clientSocket.getInputStream();
+ OutputStream serverOutputStream = serverSocket.getOutputStream();
+ assertEquals(0, clientInputStream.available());
+ serverOutputStream.write(buffer);
+ assertEquals(50, serverInputStream.available());
+
+ serverSocket.close();
+ }
+ }
+
+ // http://b/34095140
+ public void testLocalSocketCreatedFromFileDescriptor() throws Exception {
+ String address = ADDRESS_PREFIX + "_testLocalSocketCreatedFromFileDescriptor";
+
+ // Establish connection between a local client and server to get a valid client socket file
+ // descriptor.
+ try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
+ // Extract the client FileDescriptor we can use.
+ FileDescriptor fileDescriptor = socketPair.clientSocket.getFileDescriptor();
+ assertTrue(fileDescriptor.valid());
+
+ // Create the LocalSocket we want to test.
+ LocalSocket clientSocketCreatedFromFileDescriptor =
+ LocalSocket.createConnectedLocalSocket(fileDescriptor);
+ assertTrue(clientSocketCreatedFromFileDescriptor.isConnected());
+ assertTrue(clientSocketCreatedFromFileDescriptor.isBound());
+
+ // Test the LocalSocket can be used for communication.
+ LocalSocket serverSocket = socketPair.serverSocket.accept();
+ OutputStream clientOutputStream =
+ clientSocketCreatedFromFileDescriptor.getOutputStream();
+ InputStream serverInputStream = serverSocket.getInputStream();
+
+ clientOutputStream.write(12);
+ assertEquals(12, serverInputStream.read());
+
+ // Closing clientSocketCreatedFromFileDescriptor does not close the file descriptor.
+ clientSocketCreatedFromFileDescriptor.close();
+ assertTrue(fileDescriptor.valid());
+
+ // .. while closing the LocalSocket that owned the file descriptor does.
+ socketPair.clientSocket.close();
+ assertFalse(fileDescriptor.valid());
+ }
+ }
+
+ public void testFlush() throws Exception {
+ String address = ADDRESS_PREFIX + "_testFlush";
+
+ try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
+ LocalSocket clientSocket = socketPair.clientSocket;
+ LocalSocket serverSocket = socketPair.serverSocket.accept();
+
+ OutputStream clientOutputStream = clientSocket.getOutputStream();
+ InputStream serverInputStream = serverSocket.getInputStream();
+ testFlushWorks(clientOutputStream, serverInputStream);
+
+ OutputStream serverOutputStream = serverSocket.getOutputStream();
+ InputStream clientInputStream = clientSocket.getInputStream();
+ testFlushWorks(serverOutputStream, clientInputStream);
+
+ serverSocket.close();
+ }
+ }
+
+ private void testFlushWorks(OutputStream outputStream, InputStream inputStream)
+ throws Exception {
+ final int bytesToTransfer = 50;
+ StreamReader inputStreamReader = new StreamReader(inputStream, bytesToTransfer);
+
+ byte[] buffer = new byte[bytesToTransfer];
+ outputStream.write(buffer);
+ assertEquals(bytesToTransfer, inputStream.available());
+
+ // Start consuming the data.
+ inputStreamReader.start();
+
+ // This doesn't actually flush any buffers, it just polls until the reader has read all the
+ // bytes.
+ outputStream.flush();
+
+ inputStreamReader.waitForCompletion(5000);
+ inputStreamReader.assertBytesRead(bytesToTransfer);
+ assertEquals(0, inputStream.available());
+ }
+
+ private static class StreamReader extends Thread {
+ private final InputStream is;
+ private final int expectedByteCount;
+ private final CountDownLatch completeLatch = new CountDownLatch(1);
+
+ private volatile Exception exception;
+ private int bytesRead;
+
+ private StreamReader(InputStream is, int expectedByteCount) {
+ this.is = is;
+ this.expectedByteCount = expectedByteCount;
+ }
+
+ @Override
+ public void run() {
+ try {
+ byte[] buffer = new byte[10];
+ int readCount;
+ while ((readCount = is.read(buffer)) >= 0) {
+ bytesRead += readCount;
+ if (bytesRead >= expectedByteCount) {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ exception = e;
+ } finally {
+ completeLatch.countDown();
+ }
+ }
+
+ public void waitForCompletion(long waitMillis) throws Exception {
+ if (!completeLatch.await(waitMillis, TimeUnit.MILLISECONDS)) {
+ fail("Timeout waiting for completion");
+ }
+ if (exception != null) {
+ throw new Exception("Read failed", exception);
+ }
+ }
+
+ public void assertBytesRead(int expected) {
+ assertEquals(expected, bytesRead);
+ }
+ }
+
+ private static class Result {
+ private final String type;
+ private final Exception e;
+
+ private Result(String type, Exception e) {
+ this.type = type;
+ this.e = e;
+ }
+
+ static Result noException(String description) {
+ return new Result(description, null);
+ }
+
+ static Result exception(Exception e) {
+ return new Result(e.getClass().getName(), e);
+ }
+
+ void assertThrewIOException(String expectedMessage) {
+ assertEquals("Unexpected result type", IOException.class.getName(), type);
+ assertEquals("Unexpected exception message", expectedMessage, e.getMessage());
+ }
+ }
+
+ private static Result runInSeparateThread(int allowedTime, final Callable<Result> callable)
+ throws Exception {
+ ExecutorService service = Executors.newSingleThreadScheduledExecutor();
+ Future<Result> future = service.submit(callable);
+ Result result = future.get(allowedTime, TimeUnit.MILLISECONDS);
+ if (!future.isDone()) {
+ fail("Worker thread appears blocked");
+ }
+ return result;
+ }
+
+ private static class LocalSocketPair implements AutoCloseable {
+ static LocalSocketPair createConnectedSocketPair(String address) throws Exception {
+ LocalServerSocket localServerSocket = new LocalServerSocket(address);
+ final LocalSocket clientSocket = new LocalSocket();
+
+ // Establish connection between client and server
+ LocalSocketAddress locSockAddr = new LocalSocketAddress(address);
+ clientSocket.connect(locSockAddr);
+ assertTrue(clientSocket.isConnected());
+ return new LocalSocketPair(localServerSocket, clientSocket);
+ }
+
+ final LocalServerSocket serverSocket;
+ final LocalSocket clientSocket;
+
+ LocalSocketPair(LocalServerSocket serverSocket, LocalSocket clientSocket) {
+ this.serverSocket = serverSocket;
+ this.clientSocket = clientSocket;
+ }
+
+ public void close() throws Exception {
+ serverSocket.close();
+ clientSocket.close();
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/MacAddressTest.java b/tests/cts/net/src/android/net/cts/MacAddressTest.java
new file mode 100644
index 0000000..3fd3bba
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/MacAddressTest.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2018 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 static android.net.MacAddress.TYPE_BROADCAST;
+import static android.net.MacAddress.TYPE_MULTICAST;
+import static android.net.MacAddress.TYPE_UNICAST;
+
+import static com.android.testutils.ParcelUtils.assertParcelSane;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.MacAddress;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.util.Arrays;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MacAddressTest {
+
+ static class TestCase {
+ final String macAddress;
+ final String ouiString;
+ final int addressType;
+ final boolean isLocallyAssigned;
+
+ TestCase(String macAddress, String ouiString, int addressType, boolean isLocallyAssigned) {
+ this.macAddress = macAddress;
+ this.ouiString = ouiString;
+ this.addressType = addressType;
+ this.isLocallyAssigned = isLocallyAssigned;
+ }
+ }
+
+ static final boolean LOCALLY_ASSIGNED = true;
+ static final boolean GLOBALLY_UNIQUE = false;
+
+ static String typeToString(int addressType) {
+ switch (addressType) {
+ case TYPE_UNICAST:
+ return "TYPE_UNICAST";
+ case TYPE_BROADCAST:
+ return "TYPE_BROADCAST";
+ case TYPE_MULTICAST:
+ return "TYPE_MULTICAST";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ static String localAssignedToString(boolean isLocallyAssigned) {
+ return isLocallyAssigned ? "LOCALLY_ASSIGNED" : "GLOBALLY_UNIQUE";
+ }
+
+ @Test
+ public void testMacAddress() {
+ TestCase[] tests = {
+ new TestCase("ff:ff:ff:ff:ff:ff", "ff:ff:ff", TYPE_BROADCAST, LOCALLY_ASSIGNED),
+ new TestCase("d2:c4:22:4d:32:a8", "d2:c4:22", TYPE_UNICAST, LOCALLY_ASSIGNED),
+ new TestCase("33:33:aa:bb:cc:dd", "33:33:aa", TYPE_MULTICAST, LOCALLY_ASSIGNED),
+ new TestCase("06:00:00:00:00:00", "06:00:00", TYPE_UNICAST, LOCALLY_ASSIGNED),
+ new TestCase("07:00:d3:56:8a:c4", "07:00:d3", TYPE_MULTICAST, LOCALLY_ASSIGNED),
+ new TestCase("00:01:44:55:66:77", "00:01:44", TYPE_UNICAST, GLOBALLY_UNIQUE),
+ new TestCase("08:00:22:33:44:55", "08:00:22", TYPE_UNICAST, GLOBALLY_UNIQUE),
+ };
+
+ for (TestCase tc : tests) {
+ MacAddress mac = MacAddress.fromString(tc.macAddress);
+
+ if (!tc.ouiString.equals(mac.toOuiString())) {
+ fail(String.format("expected OUI string %s, got %s",
+ tc.ouiString, mac.toOuiString()));
+ }
+
+ if (tc.isLocallyAssigned != mac.isLocallyAssigned()) {
+ fail(String.format("expected %s to be %s, got %s", mac,
+ localAssignedToString(tc.isLocallyAssigned),
+ localAssignedToString(mac.isLocallyAssigned())));
+ }
+
+ if (tc.addressType != mac.getAddressType()) {
+ fail(String.format("expected %s address type to be %s, got %s", mac,
+ typeToString(tc.addressType), typeToString(mac.getAddressType())));
+ }
+
+ if (!tc.macAddress.equals(mac.toString())) {
+ fail(String.format("expected toString() to return %s, got %s",
+ tc.macAddress, mac.toString()));
+ }
+
+ if (!mac.equals(MacAddress.fromBytes(mac.toByteArray()))) {
+ byte[] bytes = mac.toByteArray();
+ fail(String.format("expected mac address from bytes %s to be %s, got %s",
+ Arrays.toString(bytes),
+ MacAddress.fromBytes(bytes),
+ mac));
+ }
+ }
+ }
+
+ @Test
+ public void testConstructorInputValidation() {
+ String[] invalidStringAddresses = {
+ "",
+ "abcd",
+ "1:2:3:4:5",
+ "1:2:3:4:5:6:7",
+ "10000:2:3:4:5:6",
+ };
+
+ for (String s : invalidStringAddresses) {
+ try {
+ MacAddress mac = MacAddress.fromString(s);
+ fail("MacAddress.fromString(" + s + ") should have failed, but returned " + mac);
+ } catch (IllegalArgumentException excepted) {
+ }
+ }
+
+ try {
+ MacAddress mac = MacAddress.fromString(null);
+ fail("MacAddress.fromString(null) should have failed, but returned " + mac);
+ } catch (NullPointerException excepted) {
+ }
+
+ byte[][] invalidBytesAddresses = {
+ {},
+ {1,2,3,4,5},
+ {1,2,3,4,5,6,7},
+ };
+
+ for (byte[] b : invalidBytesAddresses) {
+ try {
+ MacAddress mac = MacAddress.fromBytes(b);
+ fail("MacAddress.fromBytes(" + Arrays.toString(b)
+ + ") should have failed, but returned " + mac);
+ } catch (IllegalArgumentException excepted) {
+ }
+ }
+
+ try {
+ MacAddress mac = MacAddress.fromBytes(null);
+ fail("MacAddress.fromBytes(null) should have failed, but returned " + mac);
+ } catch (NullPointerException excepted) {
+ }
+ }
+
+ @Test
+ public void testMatches() {
+ // match 4 bytes prefix
+ assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+ MacAddress.fromString("aa:bb:cc:dd:00:00"),
+ MacAddress.fromString("ff:ff:ff:ff:00:00")));
+
+ // match bytes 0,1,2 and 5
+ assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+ MacAddress.fromString("aa:bb:cc:00:00:11"),
+ MacAddress.fromString("ff:ff:ff:00:00:ff")));
+
+ // match 34 bit prefix
+ assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+ MacAddress.fromString("aa:bb:cc:dd:c0:00"),
+ MacAddress.fromString("ff:ff:ff:ff:c0:00")));
+
+ // fail to match 36 bit prefix
+ assertFalse(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+ MacAddress.fromString("aa:bb:cc:dd:40:00"),
+ MacAddress.fromString("ff:ff:ff:ff:f0:00")));
+
+ // match all 6 bytes
+ assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+ MacAddress.fromString("aa:bb:cc:dd:ee:11"),
+ MacAddress.fromString("ff:ff:ff:ff:ff:ff")));
+
+ // match none of 6 bytes
+ assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+ MacAddress.fromString("00:00:00:00:00:00"),
+ MacAddress.fromString("00:00:00:00:00:00")));
+ }
+
+ /**
+ * Tests that link-local address generation from MAC is valid.
+ */
+ @Test
+ public void testLinkLocalFromMacGeneration() {
+ final MacAddress mac = MacAddress.fromString("52:74:f2:b1:a8:7f");
+ final byte[] inet6ll = {(byte) 0xfe, (byte) 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50,
+ 0x74, (byte) 0xf2, (byte) 0xff, (byte) 0xfe, (byte) 0xb1, (byte) 0xa8, 0x7f};
+ final Inet6Address llv6 = mac.getLinkLocalIpv6FromEui48Mac();
+ assertTrue(llv6.isLinkLocalAddress());
+ assertArrayEquals(inet6ll, llv6.getAddress());
+ }
+
+ @Test
+ public void testParcelMacAddress() {
+ final MacAddress mac = MacAddress.fromString("52:74:f2:b1:a8:7f");
+
+ assertParcelSane(mac, 1);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/MailToTest.java b/tests/cts/net/src/android/net/cts/MailToTest.java
new file mode 100644
index 0000000..e454d20
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/MailToTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2008 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.MailTo;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+public class MailToTest extends AndroidTestCase {
+ private static final String MAILTOURI_1 = "mailto:chris@example.com";
+ private static final String MAILTOURI_2 = "mailto:infobot@example.com?subject=current-issue";
+ private static final String MAILTOURI_3 =
+ "mailto:infobot@example.com?body=send%20current-issue";
+ private static final String MAILTOURI_4 = "mailto:infobot@example.com?body=send%20current-" +
+ "issue%0D%0Asend%20index";
+ private static final String MAILTOURI_5 = "mailto:joe@example.com?" +
+ "cc=bob@example.com&body=hello";
+ private static final String MAILTOURI_6 = "mailto:?to=joe@example.com&" +
+ "cc=bob@example.com&body=hello";
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ public void testParseMailToURI() {
+ assertFalse(MailTo.isMailTo(null));
+ assertFalse(MailTo.isMailTo(""));
+ assertFalse(MailTo.isMailTo("http://www.google.com"));
+
+ assertTrue(MailTo.isMailTo(MAILTOURI_1));
+ MailTo mailTo_1 = MailTo.parse(MAILTOURI_1);
+ Log.d("Trace", mailTo_1.toString());
+ assertEquals("chris@example.com", mailTo_1.getTo());
+ assertEquals(1, mailTo_1.getHeaders().size());
+ assertNull(mailTo_1.getBody());
+ assertNull(mailTo_1.getCc());
+ assertNull(mailTo_1.getSubject());
+ assertEquals("mailto:?to=chris%40example.com&", mailTo_1.toString());
+
+ assertTrue(MailTo.isMailTo(MAILTOURI_2));
+ MailTo mailTo_2 = MailTo.parse(MAILTOURI_2);
+ Log.d("Trace", mailTo_2.toString());
+ assertEquals(2, mailTo_2.getHeaders().size());
+ assertEquals("infobot@example.com", mailTo_2.getTo());
+ assertEquals("current-issue", mailTo_2.getSubject());
+ assertNull(mailTo_2.getBody());
+ assertNull(mailTo_2.getCc());
+ String stringUrl = mailTo_2.toString();
+ assertTrue(stringUrl.startsWith("mailto:?"));
+ assertTrue(stringUrl.contains("to=infobot%40example.com&"));
+ assertTrue(stringUrl.contains("subject=current-issue&"));
+
+ assertTrue(MailTo.isMailTo(MAILTOURI_3));
+ MailTo mailTo_3 = MailTo.parse(MAILTOURI_3);
+ Log.d("Trace", mailTo_3.toString());
+ assertEquals(2, mailTo_3.getHeaders().size());
+ assertEquals("infobot@example.com", mailTo_3.getTo());
+ assertEquals("send current-issue", mailTo_3.getBody());
+ assertNull(mailTo_3.getCc());
+ assertNull(mailTo_3.getSubject());
+ stringUrl = mailTo_3.toString();
+ assertTrue(stringUrl.startsWith("mailto:?"));
+ assertTrue(stringUrl.contains("to=infobot%40example.com&"));
+ assertTrue(stringUrl.contains("body=send%20current-issue&"));
+
+ assertTrue(MailTo.isMailTo(MAILTOURI_4));
+ MailTo mailTo_4 = MailTo.parse(MAILTOURI_4);
+ Log.d("Trace", mailTo_4.toString() + " " + mailTo_4.getBody());
+ assertEquals(2, mailTo_4.getHeaders().size());
+ assertEquals("infobot@example.com", mailTo_4.getTo());
+ assertEquals("send current-issue\r\nsend index", mailTo_4.getBody());
+ assertNull(mailTo_4.getCc());
+ assertNull(mailTo_4.getSubject());
+ stringUrl = mailTo_4.toString();
+ assertTrue(stringUrl.startsWith("mailto:?"));
+ assertTrue(stringUrl.contains("to=infobot%40example.com&"));
+ assertTrue(stringUrl.contains("body=send%20current-issue%0D%0Asend%20index&"));
+
+
+ assertTrue(MailTo.isMailTo(MAILTOURI_5));
+ MailTo mailTo_5 = MailTo.parse(MAILTOURI_5);
+ Log.d("Trace", mailTo_5.toString() + mailTo_5.getHeaders().toString()
+ + mailTo_5.getHeaders().size());
+ assertEquals(3, mailTo_5.getHeaders().size());
+ assertEquals("joe@example.com", mailTo_5.getTo());
+ assertEquals("bob@example.com", mailTo_5.getCc());
+ assertEquals("hello", mailTo_5.getBody());
+ assertNull(mailTo_5.getSubject());
+ stringUrl = mailTo_5.toString();
+ assertTrue(stringUrl.startsWith("mailto:?"));
+ assertTrue(stringUrl.contains("cc=bob%40example.com&"));
+ assertTrue(stringUrl.contains("body=hello&"));
+ assertTrue(stringUrl.contains("to=joe%40example.com&"));
+
+ assertTrue(MailTo.isMailTo(MAILTOURI_6));
+ MailTo mailTo_6 = MailTo.parse(MAILTOURI_6);
+ Log.d("Trace", mailTo_6.toString() + mailTo_6.getHeaders().toString()
+ + mailTo_6.getHeaders().size());
+ assertEquals(3, mailTo_6.getHeaders().size());
+ assertEquals(", joe@example.com", mailTo_6.getTo());
+ assertEquals("bob@example.com", mailTo_6.getCc());
+ assertEquals("hello", mailTo_6.getBody());
+ assertNull(mailTo_6.getSubject());
+ stringUrl = mailTo_6.toString();
+ assertTrue(stringUrl.startsWith("mailto:?"));
+ assertTrue(stringUrl.contains("cc=bob%40example.com&"));
+ assertTrue(stringUrl.contains("body=hello&"));
+ assertTrue(stringUrl.contains("to=%2C%20joe%40example.com&"));
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
new file mode 100644
index 0000000..691ab99
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2015 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 static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+
+import android.content.Context;
+import android.content.ContentResolver;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkUtils;
+import android.net.cts.util.CtsNetUtils;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.test.AndroidTestCase;
+
+import java.util.ArrayList;
+
+public class MultinetworkApiTest extends AndroidTestCase {
+
+ static {
+ System.loadLibrary("nativemultinetwork_jni");
+ }
+
+ private static final String TAG = "MultinetworkNativeApiTest";
+ static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
+
+ /**
+ * @return 0 on success
+ */
+ private static native int runGetaddrinfoCheck(long networkHandle);
+ private static native int runSetprocnetwork(long networkHandle);
+ private static native int runSetsocknetwork(long networkHandle);
+ private static native int runDatagramCheck(long networkHandle);
+ private static native void runResNapiMalformedCheck(long networkHandle);
+ private static native void runResNcancelCheck(long networkHandle);
+ private static native void runResNqueryCheck(long networkHandle);
+ private static native void runResNsendCheck(long networkHandle);
+ private static native void runResNnxDomainCheck(long networkHandle);
+
+
+ private ContentResolver mCR;
+ private ConnectivityManager mCM;
+ private CtsNetUtils mCtsNetUtils;
+ private String mOldMode;
+ private String mOldDnsSpecifier;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mCM = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ mCR = getContext().getContentResolver();
+ mCtsNetUtils = new CtsNetUtils(getContext());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ private Network[] getTestableNetworks() {
+ final ArrayList<Network> testableNetworks = new ArrayList<Network>();
+ for (Network network : mCM.getAllNetworks()) {
+ final NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+ if (nc != null
+ && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
+ testableNetworks.add(network);
+ }
+ }
+
+ assertTrue(
+ "This test requires that at least one network be connected. " +
+ "Please ensure that the device is connected to a network.",
+ testableNetworks.size() >= 1);
+ return testableNetworks.toArray(new Network[0]);
+ }
+
+ public void testGetaddrinfo() throws ErrnoException {
+ for (Network network : getTestableNetworks()) {
+ int errno = runGetaddrinfoCheck(network.getNetworkHandle());
+ if (errno != 0) {
+ throw new ErrnoException(
+ "getaddrinfo on " + mCM.getNetworkInfo(network), -errno);
+ }
+ }
+ }
+
+ @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
+ public void testSetprocnetwork() throws ErrnoException {
+ // Hopefully no prior test in this process space has set a default network.
+ assertNull(mCM.getProcessDefaultNetwork());
+ assertEquals(0, NetworkUtils.getBoundNetworkForProcess());
+
+ for (Network network : getTestableNetworks()) {
+ mCM.setProcessDefaultNetwork(null);
+ assertNull(mCM.getProcessDefaultNetwork());
+
+ int errno = runSetprocnetwork(network.getNetworkHandle());
+ if (errno != 0) {
+ throw new ErrnoException(
+ "setprocnetwork on " + mCM.getNetworkInfo(network), -errno);
+ }
+ Network processDefault = mCM.getProcessDefaultNetwork();
+ assertNotNull(processDefault);
+ assertEquals(network, processDefault);
+ // TODO: open DatagramSockets, connect them to 192.0.2.1 and 2001:db8::,
+ // and ensure that the source address is in fact on this network as
+ // determined by mCM.getLinkProperties(network).
+
+ mCM.setProcessDefaultNetwork(null);
+ }
+
+ for (Network network : getTestableNetworks()) {
+ NetworkUtils.bindProcessToNetwork(0);
+ assertNull(mCM.getBoundNetworkForProcess());
+
+ int errno = runSetprocnetwork(network.getNetworkHandle());
+ if (errno != 0) {
+ throw new ErrnoException(
+ "setprocnetwork on " + mCM.getNetworkInfo(network), -errno);
+ }
+ assertEquals(network, new Network(mCM.getBoundNetworkForProcess()));
+ // TODO: open DatagramSockets, connect them to 192.0.2.1 and 2001:db8::,
+ // and ensure that the source address is in fact on this network as
+ // determined by mCM.getLinkProperties(network).
+
+ NetworkUtils.bindProcessToNetwork(0);
+ }
+ }
+
+ @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
+ public void testSetsocknetwork() throws ErrnoException {
+ for (Network network : getTestableNetworks()) {
+ int errno = runSetsocknetwork(network.getNetworkHandle());
+ if (errno != 0) {
+ throw new ErrnoException(
+ "setsocknetwork on " + mCM.getNetworkInfo(network), -errno);
+ }
+ }
+ }
+
+ public void testNativeDatagramTransmission() throws ErrnoException {
+ for (Network network : getTestableNetworks()) {
+ int errno = runDatagramCheck(network.getNetworkHandle());
+ if (errno != 0) {
+ throw new ErrnoException(
+ "DatagramCheck on " + mCM.getNetworkInfo(network), -errno);
+ }
+ }
+ }
+
+ public void testNoSuchNetwork() {
+ final Network eNoNet = new Network(54321);
+ assertNull(mCM.getNetworkInfo(eNoNet));
+
+ final long eNoNetHandle = eNoNet.getNetworkHandle();
+ assertEquals(-OsConstants.ENONET, runSetsocknetwork(eNoNetHandle));
+ assertEquals(-OsConstants.ENONET, runSetprocnetwork(eNoNetHandle));
+ // TODO: correct test permissions so this call is not silently re-mapped
+ // to query on the default network.
+ // assertEquals(-OsConstants.ENONET, runGetaddrinfoCheck(eNoNetHandle));
+ }
+
+ public void testNetworkHandle() {
+ // Test Network -> NetworkHandle -> Network results in the same Network.
+ for (Network network : getTestableNetworks()) {
+ long networkHandle = network.getNetworkHandle();
+ Network newNetwork = Network.fromNetworkHandle(networkHandle);
+ assertEquals(newNetwork, network);
+ }
+
+ // Test that only obfuscated handles are allowed.
+ try {
+ Network.fromNetworkHandle(100);
+ fail();
+ } catch (IllegalArgumentException e) {}
+ try {
+ Network.fromNetworkHandle(-1);
+ fail();
+ } catch (IllegalArgumentException e) {}
+ try {
+ Network.fromNetworkHandle(0);
+ fail();
+ } catch (IllegalArgumentException e) {}
+ }
+
+ public void testResNApi() throws Exception {
+ final Network[] testNetworks = getTestableNetworks();
+
+ for (Network network : testNetworks) {
+ // Throws AssertionError directly in jni function if test fail.
+ runResNqueryCheck(network.getNetworkHandle());
+ runResNsendCheck(network.getNetworkHandle());
+ runResNcancelCheck(network.getNetworkHandle());
+ runResNapiMalformedCheck(network.getNetworkHandle());
+
+ final NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+ // Some cellular networks configure their DNS servers never to return NXDOMAIN, so don't
+ // test NXDOMAIN on these DNS servers.
+ // b/144521720
+ if (nc != null && !nc.hasTransport(TRANSPORT_CELLULAR)) {
+ runResNnxDomainCheck(network.getNetworkHandle());
+ }
+ }
+ }
+
+ @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
+ public void testResNApiNXDomainPrivateDns() throws InterruptedException {
+ mCtsNetUtils.storePrivateDnsSetting();
+ // Enable private DNS strict mode and set server to dns.google before doing NxDomain test.
+ // b/144521720
+ try {
+ mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
+ for (Network network : getTestableNetworks()) {
+ // Wait for private DNS setting to propagate.
+ mCtsNetUtils.awaitPrivateDnsSetting("NxDomain test wait private DNS setting timeout",
+ network, GOOGLE_PRIVATE_DNS_SERVER, true);
+ runResNnxDomainCheck(network.getNetworkHandle());
+ }
+ } finally {
+ mCtsNetUtils.restorePrivateDnsSetting();
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
new file mode 100644
index 0000000..7508228
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -0,0 +1,668 @@
+/*
+ * Copyright (C) 2020 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.app.Instrumentation
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.KeepalivePacketData
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.Network
+import android.net.NetworkAgent
+import android.net.NetworkAgent.CMD_ADD_KEEPALIVE_PACKET_FILTER
+import android.net.NetworkAgent.CMD_PREVENT_AUTOMATIC_RECONNECT
+import android.net.NetworkAgent.CMD_REMOVE_KEEPALIVE_PACKET_FILTER
+import android.net.NetworkAgent.CMD_REPORT_NETWORK_STATUS
+import android.net.NetworkAgent.CMD_SAVE_ACCEPT_UNVALIDATED
+import android.net.NetworkAgent.CMD_START_SOCKET_KEEPALIVE
+import android.net.NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE
+import android.net.NetworkAgent.INVALID_NETWORK
+import android.net.NetworkAgent.VALID_NETWORK
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkInfo
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.net.SocketKeepalive
+import android.net.StringNetworkSpecifier
+import android.net.Uri
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnBandwidthUpdateRequested
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnNetworkUnwanted
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnRemoveKeepalivePacketFilter
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnSaveAcceptUnvalidated
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnSignalStrengthThresholdsUpdated
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnStartSocketKeepalive
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnStopSocketKeepalive
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnValidationStatus
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Message
+import android.os.Messenger
+import androidx.test.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.internal.util.AsyncChannel
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.After
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.argThat
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import java.net.InetAddress
+import java.time.Duration
+import java.util.UUID
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+// This test doesn't really have a constraint on how fast the methods should return. If it's
+// going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio
+// without affecting the run time of successful runs. Thus, set a very high timeout.
+private const val DEFAULT_TIMEOUT_MS = 5000L
+// When waiting for a NetworkCallback to determine there was no timeout, waiting is the
+// only possible thing (the relevant handler is the one in the real ConnectivityService,
+// and then there is the Binder call), so have a short timeout for this as it will be
+// exhausted every time.
+private const val NO_CALLBACK_TIMEOUT = 200L
+// Any legal score (0~99) for the test network would do, as it is going to be kept up by the
+// requests filed by the test and should never match normal internet requests. 70 is the default
+// score of Ethernet networks, it's as good a value as any other.
+private const val TEST_NETWORK_SCORE = 70
+private const val BETTER_NETWORK_SCORE = 75
+private const val FAKE_NET_ID = 1098
+private val instrumentation: Instrumentation
+ get() = InstrumentationRegistry.getInstrumentation()
+private val realContext: Context
+ get() = InstrumentationRegistry.getContext()
+private fun Message(what: Int, arg1: Int, arg2: Int, obj: Any?) = Message.obtain().also {
+ it.what = what
+ it.arg1 = arg1
+ it.arg2 = arg2
+ it.obj = obj
+}
+
+@RunWith(AndroidJUnit4::class)
+class NetworkAgentTest {
+ @Rule @JvmField
+ val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
+
+ private val LOCAL_IPV4_ADDRESS = InetAddress.parseNumericAddress("192.0.2.1")
+ private val REMOTE_IPV4_ADDRESS = InetAddress.parseNumericAddress("192.0.2.2")
+
+ private val mCM = realContext.getSystemService(ConnectivityManager::class.java)
+ private val mHandlerThread = HandlerThread("${javaClass.simpleName} handler thread")
+ private val mFakeConnectivityService by lazy { FakeConnectivityService(mHandlerThread.looper) }
+
+ private class Provider(context: Context, looper: Looper) :
+ NetworkProvider(context, looper, "NetworkAgentTest NetworkProvider")
+
+ private val agentsToCleanUp = mutableListOf<NetworkAgent>()
+ private val callbacksToCleanUp = mutableListOf<TestableNetworkCallback>()
+
+ @Before
+ fun setUp() {
+ instrumentation.getUiAutomation().adoptShellPermissionIdentity()
+ mHandlerThread.start()
+ }
+
+ @After
+ fun tearDown() {
+ agentsToCleanUp.forEach { it.unregister() }
+ callbacksToCleanUp.forEach { mCM.unregisterNetworkCallback(it) }
+ mHandlerThread.quitSafely()
+ instrumentation.getUiAutomation().dropShellPermissionIdentity()
+ }
+
+ /**
+ * A fake that helps simulating ConnectivityService talking to a harnessed agent.
+ * This fake only supports speaking to one harnessed agent at a time because it
+ * only keeps track of one async channel.
+ */
+ private class FakeConnectivityService(looper: Looper) {
+ private val CMD_EXPECT_DISCONNECT = 1
+ private var disconnectExpected = false
+ private val msgHistory = ArrayTrackRecord<Message>().newReadHead()
+ private val asyncChannel = AsyncChannel()
+ private val handler = object : Handler(looper) {
+ override fun handleMessage(msg: Message) {
+ msgHistory.add(Message.obtain(msg)) // make a copy as the original will be recycled
+ when (msg.what) {
+ CMD_EXPECT_DISCONNECT -> disconnectExpected = true
+ AsyncChannel.CMD_CHANNEL_HALF_CONNECTED ->
+ asyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION)
+ AsyncChannel.CMD_CHANNEL_DISCONNECTED ->
+ if (!disconnectExpected) {
+ fail("Agent unexpectedly disconnected")
+ } else {
+ disconnectExpected = false
+ }
+ }
+ }
+ }
+
+ fun connect(agentMsngr: Messenger) = asyncChannel.connect(realContext, handler, agentMsngr)
+
+ fun disconnect() = asyncChannel.disconnect()
+
+ fun sendMessage(what: Int, arg1: Int = 0, arg2: Int = 0, obj: Any? = null) =
+ asyncChannel.sendMessage(Message(what, arg1, arg2, obj))
+
+ fun expectMessage(what: Int) =
+ assertNotNull(msgHistory.poll(DEFAULT_TIMEOUT_MS) { it.what == what })
+
+ fun willExpectDisconnectOnce() = handler.sendEmptyMessage(CMD_EXPECT_DISCONNECT)
+ }
+
+ private open class TestableNetworkAgent(
+ context: Context,
+ looper: Looper,
+ val nc: NetworkCapabilities,
+ val lp: LinkProperties,
+ conf: NetworkAgentConfig
+ ) : NetworkAgent(context, looper, TestableNetworkAgent::class.java.simpleName /* tag */,
+ nc, lp, TEST_NETWORK_SCORE, conf, Provider(context, looper)) {
+ private val history = ArrayTrackRecord<CallbackEntry>().newReadHead()
+
+ sealed class CallbackEntry {
+ object OnBandwidthUpdateRequested : CallbackEntry()
+ object OnNetworkUnwanted : CallbackEntry()
+ data class OnAddKeepalivePacketFilter(
+ val slot: Int,
+ val packet: KeepalivePacketData
+ ) : CallbackEntry()
+ data class OnRemoveKeepalivePacketFilter(val slot: Int) : CallbackEntry()
+ data class OnStartSocketKeepalive(
+ val slot: Int,
+ val interval: Int,
+ val packet: KeepalivePacketData
+ ) : CallbackEntry()
+ data class OnStopSocketKeepalive(val slot: Int) : CallbackEntry()
+ data class OnSaveAcceptUnvalidated(val accept: Boolean) : CallbackEntry()
+ object OnAutomaticReconnectDisabled : CallbackEntry()
+ data class OnValidationStatus(val status: Int, val uri: Uri?) : CallbackEntry()
+ data class OnSignalStrengthThresholdsUpdated(val thresholds: IntArray) : CallbackEntry()
+ }
+
+ fun getName(): String? = (nc.getNetworkSpecifier() as? StringNetworkSpecifier)?.specifier
+
+ override fun onBandwidthUpdateRequested() {
+ history.add(OnBandwidthUpdateRequested)
+ }
+
+ override fun onNetworkUnwanted() {
+ history.add(OnNetworkUnwanted)
+ }
+
+ override fun onAddKeepalivePacketFilter(slot: Int, packet: KeepalivePacketData) {
+ history.add(OnAddKeepalivePacketFilter(slot, packet))
+ }
+
+ override fun onRemoveKeepalivePacketFilter(slot: Int) {
+ history.add(OnRemoveKeepalivePacketFilter(slot))
+ }
+
+ override fun onStartSocketKeepalive(
+ slot: Int,
+ interval: Duration,
+ packet: KeepalivePacketData
+ ) {
+ history.add(OnStartSocketKeepalive(slot, interval.seconds.toInt(), packet))
+ }
+
+ override fun onStopSocketKeepalive(slot: Int) {
+ history.add(OnStopSocketKeepalive(slot))
+ }
+
+ override fun onSaveAcceptUnvalidated(accept: Boolean) {
+ history.add(OnSaveAcceptUnvalidated(accept))
+ }
+
+ override fun onAutomaticReconnectDisabled() {
+ history.add(OnAutomaticReconnectDisabled)
+ }
+
+ override fun onSignalStrengthThresholdsUpdated(thresholds: IntArray) {
+ history.add(OnSignalStrengthThresholdsUpdated(thresholds))
+ }
+
+ fun expectEmptySignalStrengths() {
+ expectCallback<OnSignalStrengthThresholdsUpdated>().let {
+ // intArrayOf() without arguments makes an empty array
+ assertArrayEquals(intArrayOf(), it.thresholds)
+ }
+ }
+
+ override fun onValidationStatus(status: Int, uri: Uri?) {
+ history.add(OnValidationStatus(status, uri))
+ }
+
+ // Expects the initial validation event that always occurs immediately after registering
+ // a NetworkAgent whose network does not require validation (which test networks do
+ // not, since they lack the INTERNET capability). It always contains the default argument
+ // for the URI.
+ fun expectNoInternetValidationStatus() = expectCallback<OnValidationStatus>().let {
+ assertEquals(it.status, VALID_NETWORK)
+ // The returned Uri is parsed from the empty string, which means it's an
+ // instance of the (private) Uri.StringUri. There are no real good ways
+ // to check this, the least bad is to just convert it to a string and
+ // make sure it's empty.
+ assertEquals("", it.uri.toString())
+ }
+
+ inline fun <reified T : CallbackEntry> expectCallback(): T {
+ val foundCallback = history.poll(DEFAULT_TIMEOUT_MS)
+ assertTrue(foundCallback is T, "Expected ${T::class} but found $foundCallback")
+ return foundCallback
+ }
+
+ fun assertNoCallback() {
+ assertTrue(waitForIdle(DEFAULT_TIMEOUT_MS),
+ "Handler didn't became idle after ${DEFAULT_TIMEOUT_MS}ms")
+ assertNull(history.peek())
+ }
+ }
+
+ private fun requestNetwork(request: NetworkRequest, callback: TestableNetworkCallback) {
+ mCM.requestNetwork(request, callback)
+ callbacksToCleanUp.add(callback)
+ }
+
+ private fun registerNetworkCallback(
+ request: NetworkRequest,
+ callback: TestableNetworkCallback
+ ) {
+ mCM.registerNetworkCallback(request, callback)
+ callbacksToCleanUp.add(callback)
+ }
+
+ private fun createNetworkAgent(
+ context: Context = realContext,
+ name: String? = null
+ ): TestableNetworkAgent {
+ val nc = NetworkCapabilities().apply {
+ addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+ removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+ addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+ addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ if (null != name) {
+ setNetworkSpecifier(StringNetworkSpecifier(name))
+ }
+ }
+ val lp = LinkProperties().apply {
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 0))
+ }
+ val config = NetworkAgentConfig.Builder().build()
+ return TestableNetworkAgent(context, mHandlerThread.looper, nc, lp, config).also {
+ agentsToCleanUp.add(it)
+ }
+ }
+
+ private fun createConnectedNetworkAgent(context: Context = realContext, name: String? = null):
+ Pair<TestableNetworkAgent, TestableNetworkCallback> {
+ val request: NetworkRequest = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .build()
+ val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+ requestNetwork(request, callback)
+ val agent = createNetworkAgent(context, name)
+ agent.register()
+ agent.markConnected()
+ return agent to callback
+ }
+
+ private fun createNetworkAgentWithFakeCS() = createNetworkAgent().also {
+ mFakeConnectivityService.connect(it.registerForTest(Network(FAKE_NET_ID)))
+ }
+
+ @Test
+ fun testConnectAndUnregister() {
+ val (agent, callback) = createConnectedNetworkAgent()
+ callback.expectAvailableThenValidatedCallbacks(agent.network)
+ agent.expectEmptySignalStrengths()
+ agent.expectNoInternetValidationStatus()
+ agent.unregister()
+ callback.expectCallback<Lost>(agent.network)
+ agent.expectCallback<OnNetworkUnwanted>()
+ assertFailsWith<IllegalStateException>("Must not be able to register an agent twice") {
+ agent.register()
+ }
+ }
+
+ @Test
+ fun testOnBandwidthUpdateRequested() {
+ val (agent, callback) = createConnectedNetworkAgent()
+ callback.expectAvailableThenValidatedCallbacks(agent.network)
+ agent.expectEmptySignalStrengths()
+ agent.expectNoInternetValidationStatus()
+ mCM.requestBandwidthUpdate(agent.network)
+ agent.expectCallback<OnBandwidthUpdateRequested>()
+ agent.unregister()
+ }
+
+ @Test
+ fun testSignalStrengthThresholds() {
+ val thresholds = intArrayOf(30, 50, 65)
+ val callbacks = thresholds.map { strength ->
+ val request = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .setSignalStrength(strength)
+ .build()
+ TestableNetworkCallback(DEFAULT_TIMEOUT_MS).also {
+ registerNetworkCallback(request, it)
+ }
+ }
+ createConnectedNetworkAgent().let { (agent, callback) ->
+ callback.expectAvailableThenValidatedCallbacks(agent.network)
+ agent.expectCallback<OnSignalStrengthThresholdsUpdated>().let {
+ assertArrayEquals(it.thresholds, thresholds)
+ }
+ agent.expectNoInternetValidationStatus()
+
+ // Send signal strength and check that the callbacks are called appropriately.
+ val nc = NetworkCapabilities(agent.nc)
+ nc.setSignalStrength(20)
+ agent.sendNetworkCapabilities(nc)
+ callbacks.forEach { it.assertNoCallback(NO_CALLBACK_TIMEOUT) }
+
+ nc.setSignalStrength(40)
+ agent.sendNetworkCapabilities(nc)
+ callbacks[0].expectAvailableCallbacks(agent.network)
+ callbacks[1].assertNoCallback(NO_CALLBACK_TIMEOUT)
+ callbacks[2].assertNoCallback(NO_CALLBACK_TIMEOUT)
+
+ nc.setSignalStrength(80)
+ agent.sendNetworkCapabilities(nc)
+ callbacks[0].expectCapabilitiesThat(agent.network) { it.signalStrength == 80 }
+ callbacks[1].expectAvailableCallbacks(agent.network)
+ callbacks[2].expectAvailableCallbacks(agent.network)
+
+ nc.setSignalStrength(55)
+ agent.sendNetworkCapabilities(nc)
+ callbacks[0].expectCapabilitiesThat(agent.network) { it.signalStrength == 55 }
+ callbacks[1].expectCapabilitiesThat(agent.network) { it.signalStrength == 55 }
+ callbacks[2].expectCallback<Lost>(agent.network)
+ }
+ callbacks.forEach {
+ mCM.unregisterNetworkCallback(it)
+ }
+ }
+
+ @Test
+ fun testSocketKeepalive(): Unit = createNetworkAgentWithFakeCS().let { agent ->
+ val packet = object : KeepalivePacketData(
+ LOCAL_IPV4_ADDRESS /* srcAddress */, 1234 /* srcPort */,
+ REMOTE_IPV4_ADDRESS /* dstAddress */, 4567 /* dstPort */,
+ ByteArray(100 /* size */) { it.toByte() /* init */ }) {}
+ val slot = 4
+ val interval = 37
+
+ mFakeConnectivityService.sendMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER,
+ arg1 = slot, obj = packet)
+ mFakeConnectivityService.sendMessage(CMD_START_SOCKET_KEEPALIVE,
+ arg1 = slot, arg2 = interval, obj = packet)
+
+ agent.expectCallback<OnAddKeepalivePacketFilter>().let {
+ assertEquals(it.slot, slot)
+ assertEquals(it.packet, packet)
+ }
+ agent.expectCallback<OnStartSocketKeepalive>().let {
+ assertEquals(it.slot, slot)
+ assertEquals(it.interval, interval)
+ assertEquals(it.packet, packet)
+ }
+
+ agent.assertNoCallback()
+
+ // Check that when the agent sends a keepalive event, ConnectivityService receives the
+ // expected message.
+ agent.sendSocketKeepaliveEvent(slot, SocketKeepalive.ERROR_UNSUPPORTED)
+ mFakeConnectivityService.expectMessage(NetworkAgent.EVENT_SOCKET_KEEPALIVE).let() {
+ assertEquals(slot, it.arg1)
+ assertEquals(SocketKeepalive.ERROR_UNSUPPORTED, it.arg2)
+ }
+
+ mFakeConnectivityService.sendMessage(CMD_STOP_SOCKET_KEEPALIVE, arg1 = slot)
+ mFakeConnectivityService.sendMessage(CMD_REMOVE_KEEPALIVE_PACKET_FILTER, arg1 = slot)
+ agent.expectCallback<OnStopSocketKeepalive>().let {
+ assertEquals(it.slot, slot)
+ }
+ agent.expectCallback<OnRemoveKeepalivePacketFilter>().let {
+ assertEquals(it.slot, slot)
+ }
+ }
+
+ @Test
+ fun testSendUpdates(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
+ callback.expectAvailableThenValidatedCallbacks(agent.network)
+ agent.expectEmptySignalStrengths()
+ agent.expectNoInternetValidationStatus()
+ val ifaceName = "adhocIface"
+ val lp = LinkProperties(agent.lp)
+ lp.setInterfaceName(ifaceName)
+ agent.sendLinkProperties(lp)
+ callback.expectLinkPropertiesThat(agent.network) {
+ it.getInterfaceName() == ifaceName
+ }
+ val nc = NetworkCapabilities(agent.nc)
+ nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+ agent.sendNetworkCapabilities(nc)
+ callback.expectCapabilitiesThat(agent.network) {
+ it.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+ }
+ }
+
+ @Test
+ fun testSendScore() {
+ // This test will create two networks and check that the one with the stronger
+ // score wins out for a request that matches them both.
+ // First create requests to make sure both networks are kept up, using the
+ // specifier so they are specific to each network
+ val name1 = UUID.randomUUID().toString()
+ val name2 = UUID.randomUUID().toString()
+ val request1 = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .setNetworkSpecifier(StringNetworkSpecifier(name1))
+ .build()
+ val request2 = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .setNetworkSpecifier(StringNetworkSpecifier(name2))
+ .build()
+ val callback1 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+ val callback2 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+ requestNetwork(request1, callback1)
+ requestNetwork(request2, callback2)
+
+ // Then file the interesting request
+ val request = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .build()
+ val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+ requestNetwork(request, callback)
+
+ // Connect the first Network
+ createConnectedNetworkAgent(name = name1).let { (agent1, _) ->
+ callback.expectAvailableThenValidatedCallbacks(agent1.network)
+ // Upgrade agent1 to a better score so that there is no ambiguity when
+ // agent2 connects that agent1 is still better
+ agent1.sendNetworkScore(BETTER_NETWORK_SCORE - 1)
+ // Connect the second agent
+ createConnectedNetworkAgent(name = name2).let { (agent2, _) ->
+ agent2.markConnected()
+ // The callback should not see anything yet
+ callback.assertNoCallback(NO_CALLBACK_TIMEOUT)
+ // Now update the score and expect the callback now prefers agent2
+ agent2.sendNetworkScore(BETTER_NETWORK_SCORE)
+ callback.expectCallback<Available>(agent2.network)
+ }
+ }
+
+ // tearDown() will unregister the requests and agents
+ }
+
+ @Test
+ fun testAgentStartsInConnecting() {
+ val mockContext = mock(Context::class.java)
+ val mockCm = mock(ConnectivityManager::class.java)
+ doReturn(mockCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE)
+ createConnectedNetworkAgent(mockContext)
+ verify(mockCm).registerNetworkAgent(any(Messenger::class.java),
+ argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING },
+ any(LinkProperties::class.java),
+ any(NetworkCapabilities::class.java),
+ anyInt() /* score */,
+ any(NetworkAgentConfig::class.java),
+ eq(NetworkProvider.ID_NONE))
+ }
+
+ @Test
+ fun testSetAcceptUnvalidated() {
+ createNetworkAgentWithFakeCS().let { agent ->
+ mFakeConnectivityService.sendMessage(CMD_SAVE_ACCEPT_UNVALIDATED, 1)
+ agent.expectCallback<OnSaveAcceptUnvalidated>().let {
+ assertTrue(it.accept)
+ }
+ agent.assertNoCallback()
+ }
+ }
+
+ @Test
+ fun testSetAcceptUnvalidatedPreventAutomaticReconnect() {
+ createNetworkAgentWithFakeCS().let { agent ->
+ mFakeConnectivityService.sendMessage(CMD_SAVE_ACCEPT_UNVALIDATED, 0)
+ mFakeConnectivityService.sendMessage(CMD_PREVENT_AUTOMATIC_RECONNECT)
+ agent.expectCallback<OnSaveAcceptUnvalidated>().let {
+ assertFalse(it.accept)
+ }
+ agent.expectCallback<OnAutomaticReconnectDisabled>()
+ agent.assertNoCallback()
+ // When automatic reconnect is turned off, the network is torn down and
+ // ConnectivityService sends a disconnect. This in turn causes the agent
+ // to send a DISCONNECTED message to CS.
+ mFakeConnectivityService.willExpectDisconnectOnce()
+ mFakeConnectivityService.disconnect()
+ mFakeConnectivityService.expectMessage(AsyncChannel.CMD_CHANNEL_DISCONNECTED)
+ agent.expectCallback<OnNetworkUnwanted>()
+ }
+ }
+
+ @Test
+ fun testPreventAutomaticReconnect() {
+ createNetworkAgentWithFakeCS().let { agent ->
+ mFakeConnectivityService.sendMessage(CMD_PREVENT_AUTOMATIC_RECONNECT)
+ agent.expectCallback<OnAutomaticReconnectDisabled>()
+ agent.assertNoCallback()
+ mFakeConnectivityService.willExpectDisconnectOnce()
+ mFakeConnectivityService.disconnect()
+ mFakeConnectivityService.expectMessage(AsyncChannel.CMD_CHANNEL_DISCONNECTED)
+ agent.expectCallback<OnNetworkUnwanted>()
+ }
+ }
+
+ @Test
+ fun testValidationStatus() = createNetworkAgentWithFakeCS().let { agent ->
+ val uri = Uri.parse("http://www.google.com")
+ val bundle = Bundle().apply {
+ putString(NetworkAgent.REDIRECT_URL_KEY, uri.toString())
+ }
+ mFakeConnectivityService.sendMessage(CMD_REPORT_NETWORK_STATUS,
+ arg1 = VALID_NETWORK, obj = bundle)
+ agent.expectCallback<OnValidationStatus>().let {
+ assertEquals(it.status, VALID_NETWORK)
+ assertEquals(it.uri, uri)
+ }
+
+ mFakeConnectivityService.sendMessage(CMD_REPORT_NETWORK_STATUS,
+ arg1 = INVALID_NETWORK, obj = Bundle())
+ agent.expectCallback<OnValidationStatus>().let {
+ assertEquals(it.status, INVALID_NETWORK)
+ assertNull(it.uri)
+ }
+ }
+
+ @Test
+ fun testTemporarilyUnmeteredCapability() {
+ // This test will create a networks with/without NET_CAPABILITY_TEMPORARILY_NOT_METERED
+ // and check that the callback reflects the capability changes.
+ // First create a request to make sure the network is kept up
+ val request1 = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .build()
+ val callback1 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS).also {
+ registerNetworkCallback(request1, it)
+ }
+ requestNetwork(request1, callback1)
+
+ // Then file the interesting request
+ val request = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .build()
+ val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+ requestNetwork(request, callback)
+
+ // Connect the network
+ createConnectedNetworkAgent().let { (agent, _) ->
+ callback.expectAvailableThenValidatedCallbacks(agent.network)
+
+ // Send TEMP_NOT_METERED and check that the callback is called appropriately.
+ val nc1 = NetworkCapabilities(agent.nc)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ agent.sendNetworkCapabilities(nc1)
+ callback.expectCapabilitiesThat(agent.network) {
+ it.hasCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ }
+
+ // Remove TEMP_NOT_METERED and check that the callback is called appropriately.
+ val nc2 = NetworkCapabilities(agent.nc)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ agent.sendNetworkCapabilities(nc2)
+ callback.expectCapabilitiesThat(agent.network) {
+ !it.hasCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ }
+ }
+
+ // tearDown() will unregister the requests and agents
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt b/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt
new file mode 100644
index 0000000..fa15e8f
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2020 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.os.Build
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.NetworkInfo
+import android.net.NetworkInfo.DetailedState
+import android.net.NetworkInfo.State
+import android.telephony.TelephonyManager
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.runner.RunWith
+import org.junit.Test
+
+const val TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE
+const val TYPE_WIFI = ConnectivityManager.TYPE_WIFI
+const val MOBILE_TYPE_NAME = "mobile"
+const val WIFI_TYPE_NAME = "WIFI"
+const val LTE_SUBTYPE_NAME = "LTE"
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NetworkInfoTest {
+ @Rule @JvmField
+ val ignoreRule = DevSdkIgnoreRule()
+
+ @Test
+ fun testAccessNetworkInfoProperties() {
+ val cm = InstrumentationRegistry.getInstrumentation().context
+ .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val ni = cm.getAllNetworkInfo()
+ assertTrue(ni.isNotEmpty())
+
+ for (netInfo in ni) {
+ when (netInfo.getType()) {
+ TYPE_MOBILE -> assertNetworkInfo(netInfo, MOBILE_TYPE_NAME)
+ TYPE_WIFI -> assertNetworkInfo(netInfo, WIFI_TYPE_NAME)
+ // TODO: Add BLUETOOTH_TETHER testing
+ }
+ }
+ }
+
+ private fun assertNetworkInfo(netInfo: NetworkInfo, expectedTypeName: String) {
+ assertTrue(expectedTypeName.equals(netInfo.getTypeName(), ignoreCase = true))
+ assertNotNull(netInfo.toString())
+
+ if (!netInfo.isConnectedOrConnecting()) return
+
+ assertTrue(netInfo.isAvailable())
+ if (State.CONNECTED == netInfo.getState()) {
+ assertTrue(netInfo.isConnected())
+ }
+ assertTrue(State.CONNECTING == netInfo.getState() ||
+ State.CONNECTED == netInfo.getState())
+ assertTrue(DetailedState.SCANNING == netInfo.getDetailedState() ||
+ DetailedState.CONNECTING == netInfo.getDetailedState() ||
+ DetailedState.AUTHENTICATING == netInfo.getDetailedState() ||
+ DetailedState.CONNECTED == netInfo.getDetailedState())
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ fun testConstructor() {
+ val networkInfo = NetworkInfo(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_LTE,
+ MOBILE_TYPE_NAME, LTE_SUBTYPE_NAME)
+
+ assertEquals(TYPE_MOBILE, networkInfo.type)
+ assertEquals(TelephonyManager.NETWORK_TYPE_LTE, networkInfo.subtype)
+ assertEquals(MOBILE_TYPE_NAME, networkInfo.typeName)
+ assertEquals(LTE_SUBTYPE_NAME, networkInfo.subtypeName)
+ assertEquals(DetailedState.IDLE, networkInfo.detailedState)
+ assertEquals(State.UNKNOWN, networkInfo.state)
+ assertNull(networkInfo.reason)
+ assertNull(networkInfo.extraInfo)
+
+ try {
+ NetworkInfo(ConnectivityManager.MAX_NETWORK_TYPE + 1,
+ TelephonyManager.NETWORK_TYPE_LTE, MOBILE_TYPE_NAME, LTE_SUBTYPE_NAME)
+ fail("Unexpected behavior. Network type is invalid.")
+ } catch (e: IllegalArgumentException) {
+ // Expected behavior.
+ }
+ }
+
+ @Test
+ fun testSetDetailedState() {
+ val networkInfo = NetworkInfo(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_LTE,
+ MOBILE_TYPE_NAME, LTE_SUBTYPE_NAME)
+ val reason = "TestNetworkInfo"
+ val extraReason = "setDetailedState test"
+
+ networkInfo.setDetailedState(DetailedState.CONNECTED, reason, extraReason)
+ assertEquals(DetailedState.CONNECTED, networkInfo.detailedState)
+ assertEquals(State.CONNECTED, networkInfo.state)
+ assertEquals(reason, networkInfo.reason)
+ assertEquals(extraReason, networkInfo.extraInfo)
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkInfo_DetailedStateTest.java b/tests/cts/net/src/android/net/cts/NetworkInfo_DetailedStateTest.java
new file mode 100644
index 0000000..590ce89
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkInfo_DetailedStateTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2009 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.NetworkInfo.DetailedState;
+import android.test.AndroidTestCase;
+
+public class NetworkInfo_DetailedStateTest extends AndroidTestCase {
+
+ public void testValueOf() {
+ assertEquals(DetailedState.AUTHENTICATING, DetailedState.valueOf("AUTHENTICATING"));
+ assertEquals(DetailedState.CONNECTED, DetailedState.valueOf("CONNECTED"));
+ assertEquals(DetailedState.CONNECTING, DetailedState.valueOf("CONNECTING"));
+ assertEquals(DetailedState.DISCONNECTED, DetailedState.valueOf("DISCONNECTED"));
+ assertEquals(DetailedState.DISCONNECTING, DetailedState.valueOf("DISCONNECTING"));
+ assertEquals(DetailedState.FAILED, DetailedState.valueOf("FAILED"));
+ assertEquals(DetailedState.IDLE, DetailedState.valueOf("IDLE"));
+ assertEquals(DetailedState.OBTAINING_IPADDR, DetailedState.valueOf("OBTAINING_IPADDR"));
+ assertEquals(DetailedState.SCANNING, DetailedState.valueOf("SCANNING"));
+ assertEquals(DetailedState.SUSPENDED, DetailedState.valueOf("SUSPENDED"));
+ }
+
+ public void testValues() {
+ DetailedState[] expected = DetailedState.values();
+ assertEquals(13, expected.length);
+ assertEquals(DetailedState.IDLE, expected[0]);
+ assertEquals(DetailedState.SCANNING, expected[1]);
+ assertEquals(DetailedState.CONNECTING, expected[2]);
+ assertEquals(DetailedState.AUTHENTICATING, expected[3]);
+ assertEquals(DetailedState.OBTAINING_IPADDR, expected[4]);
+ assertEquals(DetailedState.CONNECTED, expected[5]);
+ assertEquals(DetailedState.SUSPENDED, expected[6]);
+ assertEquals(DetailedState.DISCONNECTING, expected[7]);
+ assertEquals(DetailedState.DISCONNECTED, expected[8]);
+ assertEquals(DetailedState.FAILED, expected[9]);
+ assertEquals(DetailedState.BLOCKED, expected[10]);
+ assertEquals(DetailedState.VERIFYING_POOR_LINK, expected[11]);
+ assertEquals(DetailedState.CAPTIVE_PORTAL_CHECK, expected[12]);
+ }
+
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkInfo_StateTest.java b/tests/cts/net/src/android/net/cts/NetworkInfo_StateTest.java
new file mode 100644
index 0000000..5303ef1
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkInfo_StateTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2009 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.NetworkInfo.State;
+import android.test.AndroidTestCase;
+
+public class NetworkInfo_StateTest extends AndroidTestCase {
+
+ public void testValueOf() {
+ assertEquals(State.CONNECTED, State.valueOf("CONNECTED"));
+ assertEquals(State.CONNECTING, State.valueOf("CONNECTING"));
+ assertEquals(State.DISCONNECTED, State.valueOf("DISCONNECTED"));
+ assertEquals(State.DISCONNECTING, State.valueOf("DISCONNECTING"));
+ assertEquals(State.SUSPENDED, State.valueOf("SUSPENDED"));
+ assertEquals(State.UNKNOWN, State.valueOf("UNKNOWN"));
+ }
+
+ public void testValues() {
+ State[] expected = State.values();
+ assertEquals(6, expected.length);
+ assertEquals(State.CONNECTING, expected[0]);
+ assertEquals(State.CONNECTED, expected[1]);
+ assertEquals(State.SUSPENDED, expected[2]);
+ assertEquals(State.DISCONNECTING, expected[3]);
+ assertEquals(State.DISCONNECTED, expected[4]);
+ assertEquals(State.UNKNOWN, expected[5]);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
new file mode 100644
index 0000000..d118c8a
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2018 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 static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.MacAddress;
+import android.net.MatchAllNetworkSpecifier;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
+import android.net.UidRange;
+import android.net.wifi.WifiNetworkSpecifier;
+import android.os.Build;
+import android.os.PatternMatcher;
+import android.os.Process;
+import android.util.ArraySet;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class NetworkRequestTest {
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+ private static final String TEST_SSID = "TestSSID";
+ private static final String OTHER_SSID = "OtherSSID";
+ private static final int TEST_UID = 2097;
+ private static final String TEST_PACKAGE_NAME = "test.package.name";
+ private static final MacAddress ARBITRARY_ADDRESS = MacAddress.fromString("3:5:8:12:9:2");
+
+ private class LocalNetworkSpecifier extends NetworkSpecifier {
+ private final int mId;
+
+ LocalNetworkSpecifier(int id) {
+ mId = id;
+ }
+
+ @Override
+ public boolean canBeSatisfiedBy(NetworkSpecifier other) {
+ return other instanceof LocalNetworkSpecifier
+ && mId == ((LocalNetworkSpecifier) other).mId;
+ }
+ }
+
+ @Test
+ public void testCapabilities() {
+ assertTrue(new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build()
+ .hasCapability(NET_CAPABILITY_MMS));
+ assertFalse(new NetworkRequest.Builder().removeCapability(NET_CAPABILITY_MMS).build()
+ .hasCapability(NET_CAPABILITY_MMS));
+
+ final NetworkRequest nr = new NetworkRequest.Builder().clearCapabilities().build();
+ // Verify request has no capabilities
+ verifyNoCapabilities(nr);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testTemporarilyNotMeteredCapability() {
+ assertTrue(new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED).build()
+ .hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+ assertFalse(new NetworkRequest.Builder()
+ .removeCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED).build()
+ .hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+ }
+
+ private void verifyNoCapabilities(NetworkRequest nr) {
+ // NetworkCapabilities.mNetworkCapabilities is defined as type long
+ final int MAX_POSSIBLE_CAPABILITY = Long.SIZE;
+ for(int bit = 0; bit < MAX_POSSIBLE_CAPABILITY; bit++) {
+ assertFalse(nr.hasCapability(bit));
+ }
+ }
+
+ @Test
+ public void testTransports() {
+ assertTrue(new NetworkRequest.Builder().addTransportType(TRANSPORT_BLUETOOTH).build()
+ .hasTransport(TRANSPORT_BLUETOOTH));
+ assertFalse(new NetworkRequest.Builder().removeTransportType(TRANSPORT_BLUETOOTH).build()
+ .hasTransport(TRANSPORT_BLUETOOTH));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testSpecifier() {
+ assertNull(new NetworkRequest.Builder().build().getNetworkSpecifier());
+ final WifiNetworkSpecifier specifier = new WifiNetworkSpecifier.Builder()
+ .setSsidPattern(new PatternMatcher(TEST_SSID, PatternMatcher.PATTERN_LITERAL))
+ .setBssidPattern(ARBITRARY_ADDRESS, ARBITRARY_ADDRESS)
+ .build();
+ final NetworkSpecifier obtainedSpecifier = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .setNetworkSpecifier(specifier)
+ .build()
+ .getNetworkSpecifier();
+ assertEquals(obtainedSpecifier, specifier);
+
+ assertNull(new NetworkRequest.Builder()
+ .clearCapabilities()
+ .build()
+ .getNetworkSpecifier());
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testRequestorPackageName() {
+ assertNull(new NetworkRequest.Builder().build().getRequestorPackageName());
+ final String pkgName = "android.net.test";
+ final NetworkCapabilities nc = new NetworkCapabilities.Builder()
+ .setRequestorPackageName(pkgName)
+ .build();
+ final NetworkRequest nr = new NetworkRequest.Builder()
+ .setCapabilities(nc)
+ .build();
+ assertEquals(pkgName, nr.getRequestorPackageName());
+ assertNull(new NetworkRequest.Builder()
+ .clearCapabilities()
+ .build()
+ .getRequestorPackageName());
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testCanBeSatisfiedBy() {
+ final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */);
+ final LocalNetworkSpecifier specifier2 = new LocalNetworkSpecifier(5678 /* id */);
+
+ final NetworkCapabilities capCellularMmsInternet = new NetworkCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_MMS)
+ .addCapability(NET_CAPABILITY_INTERNET);
+ final NetworkCapabilities capCellularVpnMmsInternet =
+ new NetworkCapabilities(capCellularMmsInternet).addTransportType(TRANSPORT_VPN);
+ final NetworkCapabilities capCellularMmsInternetSpecifier1 =
+ new NetworkCapabilities(capCellularMmsInternet).setNetworkSpecifier(specifier1);
+ final NetworkCapabilities capVpnInternetSpecifier1 = new NetworkCapabilities()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addTransportType(TRANSPORT_VPN)
+ .setNetworkSpecifier(specifier1);
+ final NetworkCapabilities capCellularMmsInternetMatchallspecifier =
+ new NetworkCapabilities(capCellularMmsInternet)
+ .setNetworkSpecifier(new MatchAllNetworkSpecifier());
+ final NetworkCapabilities capCellularMmsInternetSpecifier2 =
+ new NetworkCapabilities(capCellularMmsInternet).setNetworkSpecifier(specifier2);
+
+ final NetworkRequest requestCellularInternetSpecifier1 = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .setNetworkSpecifier(specifier1)
+ .build();
+ assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(null));
+ assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(new NetworkCapabilities()));
+ assertTrue(requestCellularInternetSpecifier1.canBeSatisfiedBy(
+ capCellularMmsInternetMatchallspecifier));
+ assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(capCellularMmsInternet));
+ assertTrue(requestCellularInternetSpecifier1.canBeSatisfiedBy(
+ capCellularMmsInternetSpecifier1));
+ assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(capCellularVpnMmsInternet));
+ assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(
+ capCellularMmsInternetSpecifier2));
+
+ final NetworkRequest requestCellularInternet = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build();
+ assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularMmsInternet));
+ assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularMmsInternetSpecifier1));
+ assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularMmsInternetSpecifier2));
+ assertFalse(requestCellularInternet.canBeSatisfiedBy(capVpnInternetSpecifier1));
+ assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularVpnMmsInternet));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testInvariantInCanBeSatisfiedBy() {
+ // Test invariant that result of NetworkRequest.canBeSatisfiedBy() should be the same with
+ // NetworkCapabilities.satisfiedByNetworkCapabilities().
+ final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */);
+ final int uid = Process.myUid();
+ final ArraySet<UidRange> ranges = new ArraySet<>();
+ ranges.add(new UidRange(uid, uid));
+ final NetworkRequest requestCombination = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .setLinkUpstreamBandwidthKbps(1000)
+ .setNetworkSpecifier(specifier1)
+ .setSignalStrength(-123)
+ .setUids(ranges).build();
+ final NetworkCapabilities capCell = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR).build();
+ assertCorrectlySatisfies(false, requestCombination, capCell);
+
+ final NetworkCapabilities capCellInternet = new NetworkCapabilities.Builder(capCell)
+ .addCapability(NET_CAPABILITY_INTERNET).build();
+ assertCorrectlySatisfies(false, requestCombination, capCellInternet);
+
+ final NetworkCapabilities capCellInternetBW =
+ new NetworkCapabilities.Builder(capCellInternet)
+ .setLinkUpstreamBandwidthKbps(1024).build();
+ assertCorrectlySatisfies(false, requestCombination, capCellInternetBW);
+
+ final NetworkCapabilities capCellInternetBWSpecifier1 =
+ new NetworkCapabilities.Builder(capCellInternetBW)
+ .setNetworkSpecifier(specifier1).build();
+ assertCorrectlySatisfies(false, requestCombination, capCellInternetBWSpecifier1);
+
+ final NetworkCapabilities capCellInternetBWSpecifier1Signal =
+ new NetworkCapabilities.Builder(capCellInternetBWSpecifier1)
+ .setSignalStrength(-123).build();
+ assertCorrectlySatisfies(true, requestCombination,
+ capCellInternetBWSpecifier1Signal);
+
+ final NetworkCapabilities capCellInternetBWSpecifier1SignalUid =
+ new NetworkCapabilities.Builder(capCellInternetBWSpecifier1Signal)
+ .setOwnerUid(uid)
+ .setAdministratorUids(new int [] {uid}).build();
+ assertCorrectlySatisfies(true, requestCombination,
+ capCellInternetBWSpecifier1SignalUid);
+ }
+
+ private void assertCorrectlySatisfies(boolean expect, NetworkRequest request,
+ NetworkCapabilities nc) {
+ assertEquals(expect, request.canBeSatisfiedBy(nc));
+ assertEquals(
+ request.canBeSatisfiedBy(nc),
+ request.networkCapabilities.satisfiedByNetworkCapabilities(nc));
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testRequestorUid() {
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ // Verify default value is INVALID_UID
+ assertEquals(Process.INVALID_UID, new NetworkRequest.Builder()
+ .setCapabilities(nc).build().getRequestorUid());
+
+ nc.setRequestorUid(1314);
+ final NetworkRequest nr = new NetworkRequest.Builder().setCapabilities(nc).build();
+ assertEquals(1314, nr.getRequestorUid());
+
+ assertEquals(Process.INVALID_UID, new NetworkRequest.Builder()
+ .clearCapabilities().build().getRequestorUid());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkStackDependenciesTest.kt b/tests/cts/net/src/android/net/cts/NetworkStackDependenciesTest.kt
new file mode 100644
index 0000000..1a7f955
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkStackDependenciesTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2020 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.pm.PackageManager
+import android.net.cts.util.CtsNetUtils
+import android.net.wifi.WifiManager
+import android.os.Build
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+/**
+ * Basic tests for APIs used by the network stack module.
+ */
+class NetworkStackDependenciesTest {
+ @Test
+ @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
+ fun testGetFrequency() {
+ // WifiInfo#getFrequency was missing a CTS test in Q: this test is run as part of MTS on Q
+ // devices to ensure it behaves correctly.
+ val context = InstrumentationRegistry.getInstrumentation().getContext()
+ assumeTrue("This test only applies to devices that support wifi",
+ context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI))
+ val wifiManager = context.getSystemService(WifiManager::class.java)
+ assertNotNull(wifiManager, "Device supports wifi but there is no WifiManager")
+
+ CtsNetUtils(context).ensureWifiConnected()
+ val wifiInfo = wifiManager.getConnectionInfo()
+ // The NetworkStack can handle any value of getFrequency; unknown frequencies will not be
+ // classified in metrics, but this is expected behavior. It is only important that the
+ // method does not crash. Still verify that the frequency is positive
+ val frequency = wifiInfo.getFrequency()
+ assertTrue(frequency > 0, "Frequency must be > 0")
+ }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
new file mode 100644
index 0000000..1a48983
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2020 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 static android.os.Process.INVALID_UID;
+
+import static org.junit.Assert.assertEquals;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.INetworkStatsService;
+import android.net.TrafficStats;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.test.AndroidTestCase;
+import android.util.SparseArray;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.CollectionUtils;
+import com.android.testutils.DevSdkIgnoreRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+@RunWith(AndroidJUnit4.class)
+public class NetworkStatsBinderTest {
+ // NOTE: These are shamelessly copied from TrafficStats.
+ private static final int TYPE_RX_BYTES = 0;
+ private static final int TYPE_RX_PACKETS = 1;
+ private static final int TYPE_TX_BYTES = 2;
+ private static final int TYPE_TX_PACKETS = 3;
+
+ @Rule
+ public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(
+ Build.VERSION_CODES.Q /* ignoreClassUpTo */);
+
+ private final SparseArray<Function<Integer, Long>> mUidStatsQueryOpArray = new SparseArray<>();
+
+ @Before
+ public void setUp() throws Exception {
+ mUidStatsQueryOpArray.put(TYPE_RX_BYTES, uid -> TrafficStats.getUidRxBytes(uid));
+ mUidStatsQueryOpArray.put(TYPE_RX_PACKETS, uid -> TrafficStats.getUidRxPackets(uid));
+ mUidStatsQueryOpArray.put(TYPE_TX_BYTES, uid -> TrafficStats.getUidTxBytes(uid));
+ mUidStatsQueryOpArray.put(TYPE_TX_PACKETS, uid -> TrafficStats.getUidTxPackets(uid));
+ }
+
+ private long getUidStatsFromBinder(int uid, int type) throws Exception {
+ Method getServiceMethod = Class.forName("android.os.ServiceManager")
+ .getDeclaredMethod("getService", new Class[]{String.class});
+ IBinder binder = (IBinder) getServiceMethod.invoke(null, Context.NETWORK_STATS_SERVICE);
+ INetworkStatsService nss = INetworkStatsService.Stub.asInterface(binder);
+ return nss.getUidStats(uid, type);
+ }
+
+ private int getFirstAppUidThat(@NonNull Predicate<Integer> predicate) {
+ PackageManager pm = InstrumentationRegistry.getContext().getPackageManager();
+ List<PackageInfo> apps = pm.getInstalledPackages(0 /* flags */);
+ final PackageInfo match = CollectionUtils.find(apps,
+ it -> it.applicationInfo != null && predicate.test(it.applicationInfo.uid));
+ if (match != null) return match.applicationInfo.uid;
+ return INVALID_UID;
+ }
+
+ @Test
+ public void testAccessUidStatsFromBinder() throws Exception {
+ final int myUid = Process.myUid();
+ final List<Integer> testUidList = new ArrayList<>();
+
+ // Prepare uid list for testing.
+ testUidList.add(INVALID_UID);
+ testUidList.add(Process.ROOT_UID);
+ testUidList.add(Process.SYSTEM_UID);
+ testUidList.add(myUid);
+ testUidList.add(Process.LAST_APPLICATION_UID);
+ testUidList.add(Process.LAST_APPLICATION_UID + 1);
+ // If available, pick another existing uid for testing that is not already contained
+ // in the list above.
+ final int notMyUid = getFirstAppUidThat(uid -> uid >= 0 && !testUidList.contains(uid));
+ if (notMyUid != INVALID_UID) testUidList.add(notMyUid);
+
+ for (final int uid : testUidList) {
+ for (int i = 0; i < mUidStatsQueryOpArray.size(); i++) {
+ final int type = mUidStatsQueryOpArray.keyAt(i);
+ try {
+ final long uidStatsFromBinder = getUidStatsFromBinder(uid, type);
+ final long uidTrafficStats = mUidStatsQueryOpArray.get(type).apply(uid);
+
+ // Verify that UNSUPPORTED is returned if the uid is not current app uid.
+ if (uid != myUid) {
+ assertEquals(uidStatsFromBinder, TrafficStats.UNSUPPORTED);
+ }
+ // Verify that returned result is the same with the result get from
+ // TrafficStats.
+ // TODO: If the test is flaky then it should instead assert that the values
+ // are approximately similar.
+ assertEquals("uidStats is not matched for query type " + type
+ + ", uid=" + uid + ", myUid=" + myUid, uidTrafficStats,
+ uidStatsFromBinder);
+ } catch (IllegalAccessException e) {
+ /* Java language access prevents exploitation. */
+ return;
+ } catch (InvocationTargetException e) {
+ /* Underlying method has been changed. */
+ return;
+ } catch (ClassNotFoundException e) {
+ /* not vulnerable if hidden API no longer available */
+ return;
+ } catch (NoSuchMethodException e) {
+ /* not vulnerable if hidden API no longer available */
+ return;
+ } catch (RemoteException e) {
+ return;
+ }
+ }
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
new file mode 100644
index 0000000..5290f0d
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2020 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.Manifest.permission.NETWORK_SETTINGS
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.ConnectivityManager
+import android.net.EthernetManager
+import android.net.InetAddresses
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkRequest
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.net.Uri
+import android.net.dhcp.DhcpDiscoverPacket
+import android.net.dhcp.DhcpPacket
+import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE
+import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_DISCOVER
+import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_REQUEST
+import android.net.dhcp.DhcpRequestPacket
+import android.os.Build
+import android.os.HandlerThread
+import android.platform.test.annotations.AppModeFull
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress
+import com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address
+import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DhcpClientPacketFilter
+import com.android.testutils.DhcpOptionFilter
+import com.android.testutils.RecorderCallback.CallbackEntry
+import com.android.testutils.TapPacketReader
+import com.android.testutils.TestHttpServer
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
+import fi.iki.elonen.NanoHTTPD.Response.Status
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.Inet4Address
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val MAX_PACKET_LENGTH = 1500
+private const val TEST_TIMEOUT_MS = 10_000L
+
+private const val TEST_LEASE_TIMEOUT_SECS = 3600 * 12
+private const val TEST_PREFIX_LENGTH = 24
+
+private const val TEST_LOGIN_URL = "https://login.capport.android.com"
+private const val TEST_VENUE_INFO_URL = "https://venueinfo.capport.android.com"
+private const val TEST_DOMAIN_NAME = "lan"
+private const val TEST_MTU = 1500.toShort()
+
+@AppModeFull(reason = "Instant apps cannot create test networks")
+@RunWith(AndroidJUnit4::class)
+class NetworkValidationTest {
+ @JvmField
+ @Rule
+ val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
+
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ private val tnm by lazy { context.assertHasService(TestNetworkManager::class.java) }
+ private val eth by lazy { context.assertHasService(EthernetManager::class.java) }
+ private val cm by lazy { context.assertHasService(ConnectivityManager::class.java) }
+
+ private val handlerThread = HandlerThread(NetworkValidationTest::class.java.simpleName)
+ private val serverIpAddr = InetAddresses.parseNumericAddress("192.0.2.222") as Inet4Address
+ private val clientIpAddr = InetAddresses.parseNumericAddress("192.0.2.111") as Inet4Address
+ private val httpServer = TestHttpServer()
+ private val ethRequest = NetworkRequest.Builder()
+ // ETHERNET|TEST transport networks do not have NET_CAPABILITY_TRUSTED
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST).build()
+ private val ethRequestCb = TestableNetworkCallback()
+
+ private lateinit var iface: TestNetworkInterface
+ private lateinit var reader: TapPacketReader
+ private lateinit var capportUrl: Uri
+
+ private var testSkipped = false
+
+ @Before
+ fun setUp() {
+ // This test requires using a tap interface as an ethernet interface.
+ val pm = context.getPackageManager()
+ testSkipped = !pm.hasSystemFeature(PackageManager.FEATURE_ETHERNET) &&
+ context.getSystemService(EthernetManager::class.java) == null
+ assumeFalse(testSkipped)
+
+ // Register a request so the network does not get torn down
+ cm.requestNetwork(ethRequest, ethRequestCb)
+ runAsShell(NETWORK_SETTINGS, MANAGE_TEST_NETWORKS) {
+ eth.setIncludeTestInterfaces(true)
+ // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor
+ // does not go out of scope, which would cause it to close the underlying FileDescriptor
+ // in its finalizer.
+ iface = tnm.createTapInterface()
+ }
+
+ handlerThread.start()
+ reader = TapPacketReader(
+ handlerThread.threadHandler,
+ iface.fileDescriptor.fileDescriptor,
+ MAX_PACKET_LENGTH)
+ reader.startAsyncForTest()
+ httpServer.start()
+
+ // Pad the listening port to make sure it is always of length 5. This ensures the URL has
+ // always the same length so the test can use constant IP and UDP header lengths.
+ // The maximum port number is 65535 so a length of 5 is always enough.
+ capportUrl = Uri.parse("http://localhost:${httpServer.listeningPort}/testapi.html?par=val")
+ }
+
+ @After
+ fun tearDown() {
+ if (testSkipped) return
+ cm.unregisterNetworkCallback(ethRequestCb)
+
+ runAsShell(NETWORK_SETTINGS) { eth.setIncludeTestInterfaces(false) }
+
+ httpServer.stop()
+ handlerThread.threadHandler.post { reader.stop() }
+ handlerThread.quitSafely()
+
+ iface.fileDescriptor.close()
+ }
+
+ @Test
+ fun testCapportApiCallbacks() {
+ httpServer.addResponse(capportUrl, Status.OK, content = """
+ |{
+ | "captive": true,
+ | "user-portal-url": "$TEST_LOGIN_URL",
+ | "venue-info-url": "$TEST_VENUE_INFO_URL"
+ |}
+ """.trimMargin())
+
+ // Handle the DHCP handshake that includes the capport API URL
+ val discover = reader.assertDhcpPacketReceived(
+ DhcpDiscoverPacket::class.java, TEST_TIMEOUT_MS, DHCP_MESSAGE_TYPE_DISCOVER)
+ reader.sendResponse(makeOfferPacket(discover.clientMac, discover.transactionId))
+
+ val request = reader.assertDhcpPacketReceived(
+ DhcpRequestPacket::class.java, TEST_TIMEOUT_MS, DHCP_MESSAGE_TYPE_REQUEST)
+ assertEquals(discover.transactionId, request.transactionId)
+ assertEquals(clientIpAddr, request.mRequestedIp)
+ reader.sendResponse(makeAckPacket(request.clientMac, request.transactionId))
+
+ // The first request received by the server should be for the portal API
+ assertTrue(httpServer.requestsRecord.poll(TEST_TIMEOUT_MS, 0)?.matches(capportUrl) ?: false,
+ "The device did not fetch captive portal API data within timeout")
+
+ // Expect network callbacks with capport info
+ val testCb = TestableNetworkCallback(TEST_TIMEOUT_MS)
+ // LinkProperties do not contain captive portal info if the callback is registered without
+ // NETWORK_SETTINGS permissions.
+ val lp = runAsShell(NETWORK_SETTINGS) {
+ cm.registerNetworkCallback(ethRequest, testCb)
+
+ try {
+ val ncCb = testCb.eventuallyExpect<CallbackEntry.CapabilitiesChanged> {
+ it.caps.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)
+ }
+ testCb.eventuallyExpect<CallbackEntry.LinkPropertiesChanged> {
+ it.network == ncCb.network && it.lp.captivePortalData != null
+ }.lp
+ } finally {
+ cm.unregisterNetworkCallback(testCb)
+ }
+ }
+
+ assertEquals(capportUrl, lp.captivePortalApiUrl)
+ with(lp.captivePortalData) {
+ assertNotNull(this)
+ assertTrue(isCaptive)
+ assertEquals(Uri.parse(TEST_LOGIN_URL), userPortalUrl)
+ assertEquals(Uri.parse(TEST_VENUE_INFO_URL), venueInfoUrl)
+ }
+ }
+
+ private fun makeOfferPacket(clientMac: ByteArray, transactionId: Int) =
+ DhcpPacket.buildOfferPacket(DhcpPacket.ENCAP_L2, transactionId,
+ false /* broadcast */, serverIpAddr, IPV4_ADDR_ANY /* relayIp */, clientIpAddr,
+ clientMac, TEST_LEASE_TIMEOUT_SECS,
+ getPrefixMaskAsInet4Address(TEST_PREFIX_LENGTH),
+ getBroadcastAddress(clientIpAddr, TEST_PREFIX_LENGTH),
+ listOf(serverIpAddr) /* gateways */, listOf(serverIpAddr) /* dnsServers */,
+ serverIpAddr, TEST_DOMAIN_NAME, null /* hostname */, true /* metered */,
+ TEST_MTU, capportUrl.toString())
+
+ private fun makeAckPacket(clientMac: ByteArray, transactionId: Int) =
+ DhcpPacket.buildAckPacket(DhcpPacket.ENCAP_L2, transactionId,
+ false /* broadcast */, serverIpAddr, IPV4_ADDR_ANY /* relayIp */, clientIpAddr,
+ clientIpAddr /* requestClientIp */, clientMac, TEST_LEASE_TIMEOUT_SECS,
+ getPrefixMaskAsInet4Address(TEST_PREFIX_LENGTH),
+ getBroadcastAddress(clientIpAddr, TEST_PREFIX_LENGTH),
+ listOf(serverIpAddr) /* gateways */, listOf(serverIpAddr) /* dnsServers */,
+ serverIpAddr, TEST_DOMAIN_NAME, null /* hostname */, true /* metered */,
+ TEST_MTU, false /* rapidCommit */, capportUrl.toString())
+}
+
+private fun <T : DhcpPacket> TapPacketReader.assertDhcpPacketReceived(
+ packetType: Class<T>,
+ timeoutMs: Long,
+ type: Byte
+): T {
+ val packetBytes = poll(timeoutMs, DhcpClientPacketFilter()
+ .and(DhcpOptionFilter(DHCP_MESSAGE_TYPE, type)))
+ ?: fail("${packetType.simpleName} not received within timeout")
+ val packet = DhcpPacket.decodeFullPacket(packetBytes, packetBytes.size, DhcpPacket.ENCAP_L2)
+ assertTrue(packetType.isInstance(packet),
+ "Expected ${packetType.simpleName} but got ${packet.javaClass.simpleName}")
+ return packetType.cast(packet)
+}
+
+private fun <T> Context.assertHasService(manager: Class<T>): T {
+ return getSystemService(manager) ?: fail("Service $manager not found")
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
new file mode 100644
index 0000000..f6fc75b
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2020 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
+import android.net.util.NetworkStackUtils
+import android.provider.DeviceConfig
+import com.android.testutils.runAsShell
+
+/**
+ * Collection of utility methods for configuring network validation.
+ */
+internal object NetworkValidationTestUtil {
+
+ /**
+ * Clear the test network validation URLs.
+ */
+ fun clearValidationTestUrlsDeviceConfig() {
+ setHttpsUrlDeviceConfig(null)
+ setHttpUrlDeviceConfig(null)
+ setUrlExpirationDeviceConfig(null)
+ }
+
+ /**
+ * Set the test validation HTTPS URL.
+ *
+ * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL
+ */
+ fun setHttpsUrlDeviceConfig(url: String?) =
+ setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
+
+ /**
+ * Set the test validation HTTP URL.
+ *
+ * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL
+ */
+ fun setHttpUrlDeviceConfig(url: String?) =
+ setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
+
+ /**
+ * Set the test validation URL expiration.
+ *
+ * @see NetworkStackUtils.TEST_URL_EXPIRATION_TIME
+ */
+ fun setUrlExpirationDeviceConfig(timestamp: Long?) =
+ setConfig(NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
+
+ private fun setConfig(configKey: String, value: String?) {
+ runAsShell(Manifest.permission.WRITE_DEVICE_CONFIG) {
+ DeviceConfig.setProperty(
+ DeviceConfig.NAMESPACE_CONNECTIVITY, configKey, value, false /* makeDefault */)
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/NetworkWatchlistTest.java b/tests/cts/net/src/android/net/cts/NetworkWatchlistTest.java
new file mode 100644
index 0000000..81a9e30
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkWatchlistTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2018 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.platform.test.annotations.AppModeFull;
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.ApiLevelUtil;
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Formatter;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NetworkWatchlistTest {
+
+ private static final String TEST_WATCHLIST_XML = "assets/network_watchlist_config_for_test.xml";
+ private static final String TEST_EMPTY_WATCHLIST_XML =
+ "assets/network_watchlist_config_empty_for_test.xml";
+ private static final String TMP_CONFIG_PATH =
+ "/data/local/tmp/network_watchlist_config_for_test.xml";
+ // Generated from sha256sum network_watchlist_config_for_test.xml
+ private static final String TEST_WATCHLIST_CONFIG_HASH =
+ "B5FC4636994180D54E1E912F78178AB1D8BD2BE71D90CA9F5BBC3284E4D04ED4";
+
+ private ConnectivityManager mConnectivityManager;
+ private boolean mHasFeature;
+
+ @Before
+ public void setUp() throws Exception {
+ mHasFeature = isAtLeastP();
+ mConnectivityManager =
+ (ConnectivityManager) InstrumentationRegistry.getContext().getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ assumeTrue(mHasFeature);
+ // Set empty watchlist test config before testing
+ setWatchlistConfig(TEST_EMPTY_WATCHLIST_XML);
+ // Verify test watchlist config is not set before testing
+ byte[] result = mConnectivityManager.getNetworkWatchlistConfigHash();
+ assertNotNull("Watchlist config does not exist", result);
+ assertNotEquals(TEST_WATCHLIST_CONFIG_HASH, byteArrayToHexString(result));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mHasFeature) {
+ // Set empty watchlist test config after testing
+ setWatchlistConfig(TEST_EMPTY_WATCHLIST_XML);
+ }
+ }
+
+ private void cleanup() throws IOException {
+ runCommand("rm " + TMP_CONFIG_PATH);
+ }
+
+ private boolean isAtLeastP() throws Exception {
+ // TODO: replace with ApiLevelUtil.isAtLeast(Build.VERSION_CODES.P) when the P API level
+ // constant is defined.
+ return ApiLevelUtil.getCodename().compareToIgnoreCase("P") >= 0;
+ }
+
+ /**
+ * Test if ConnectivityManager.getNetworkWatchlistConfigHash() correctly
+ * returns the hash of config we set.
+ */
+ @Test
+ @AppModeFull(reason = "Cannot access resource file in instant app mode")
+ public void testGetWatchlistConfigHash() throws Exception {
+ // Set watchlist config file for test
+ setWatchlistConfig(TEST_WATCHLIST_XML);
+ // Test if watchlist config hash value is correct
+ byte[] result = mConnectivityManager.getNetworkWatchlistConfigHash();
+ Assert.assertEquals(TEST_WATCHLIST_CONFIG_HASH, byteArrayToHexString(result));
+ }
+
+ private static String byteArrayToHexString(byte[] bytes) {
+ Formatter formatter = new Formatter();
+ for (byte b : bytes) {
+ formatter.format("%02X", b);
+ }
+ return formatter.toString();
+ }
+
+ private void saveResourceToFile(String res, String filePath) throws IOException {
+ // App can't access /data/local/tmp directly, so we pipe resource to file through stdin.
+ ParcelFileDescriptor stdin = pipeFromStdin(filePath);
+ pipeResourceToFileDescriptor(res, stdin);
+ }
+
+ /* Pipe stdin to a file in filePath. Returns PFD for stdin. */
+ private ParcelFileDescriptor pipeFromStdin(String filePath) {
+ // Not all devices have symlink for /dev/stdin, so use /proc/self/fd/0 directly.
+ // /dev/stdin maps to /proc/self/fd/0.
+ return runRwCommand("cp /proc/self/fd/0 " + filePath)[1];
+ }
+
+ private void pipeResourceToFileDescriptor(String res, ParcelFileDescriptor pfd)
+ throws IOException {
+ InputStream resStream = getClass().getClassLoader().getResourceAsStream(res);
+ FileOutputStream fdStream = new ParcelFileDescriptor.AutoCloseOutputStream(pfd);
+
+ FileUtils.copy(resStream, fdStream);
+
+ try {
+ fdStream.close();
+ } catch (IOException e) {
+ }
+ }
+
+ private static String runCommand(String command) throws IOException {
+ return SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(), command);
+ }
+
+ private static ParcelFileDescriptor[] runRwCommand(String command) {
+ return InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation().executeShellCommandRw(command);
+ }
+
+ private void setWatchlistConfig(String watchlistConfigFile) throws Exception {
+ cleanup();
+ saveResourceToFile(watchlistConfigFile, TMP_CONFIG_PATH);
+ final String cmdResult = runCommand(
+ "cmd network_watchlist set-test-config " + TMP_CONFIG_PATH).trim();
+ assertThat(cmdResult).contains("Success");
+ cleanup();
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/PacketUtils.java b/tests/cts/net/src/android/net/cts/PacketUtils.java
new file mode 100644
index 0000000..0aedecb
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/PacketUtils.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2018 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 static android.system.OsConstants.IPPROTO_IPV6;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class PacketUtils {
+ private static final String TAG = PacketUtils.class.getSimpleName();
+
+ private static final int DATA_BUFFER_LEN = 4096;
+
+ static final int IP4_HDRLEN = 20;
+ static final int IP6_HDRLEN = 40;
+ static final int UDP_HDRLEN = 8;
+ static final int TCP_HDRLEN = 20;
+ static final int TCP_HDRLEN_WITH_TIMESTAMP_OPT = TCP_HDRLEN + 12;
+
+ // Not defined in OsConstants
+ static final int IPPROTO_IPV4 = 4;
+ static final int IPPROTO_ESP = 50;
+
+ // Encryption parameters
+ static final int AES_GCM_IV_LEN = 8;
+ static final int AES_CBC_IV_LEN = 16;
+ static final int AES_GCM_BLK_SIZE = 4;
+ static final int AES_CBC_BLK_SIZE = 16;
+
+ // Encryption algorithms
+ static final String AES = "AES";
+ static final String AES_CBC = "AES/CBC/NoPadding";
+ static final String HMAC_SHA_256 = "HmacSHA256";
+
+ public interface Payload {
+ byte[] getPacketBytes(IpHeader header) throws Exception;
+
+ void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) throws Exception;
+
+ short length();
+
+ int getProtocolId();
+ }
+
+ public abstract static class IpHeader {
+
+ public final byte proto;
+ public final InetAddress srcAddr;
+ public final InetAddress dstAddr;
+ public final Payload payload;
+
+ public IpHeader(int proto, InetAddress src, InetAddress dst, Payload payload) {
+ this.proto = (byte) proto;
+ this.srcAddr = src;
+ this.dstAddr = dst;
+ this.payload = payload;
+ }
+
+ public abstract byte[] getPacketBytes() throws Exception;
+
+ public abstract int getProtocolId();
+ }
+
+ public static class Ip4Header extends IpHeader {
+ private short checksum;
+
+ public Ip4Header(int proto, Inet4Address src, Inet4Address dst, Payload payload) {
+ super(proto, src, dst, payload);
+ }
+
+ public byte[] getPacketBytes() throws Exception {
+ ByteBuffer resultBuffer = buildHeader();
+ payload.addPacketBytes(this, resultBuffer);
+
+ return getByteArrayFromBuffer(resultBuffer);
+ }
+
+ public ByteBuffer buildHeader() {
+ ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+ // Version, IHL
+ bb.put((byte) (0x45));
+
+ // DCSP, ECN
+ bb.put((byte) 0);
+
+ // Total Length
+ bb.putShort((short) (IP4_HDRLEN + payload.length()));
+
+ // Empty for Identification, Flags and Fragment Offset
+ bb.putShort((short) 0);
+ bb.put((byte) 0x40);
+ bb.put((byte) 0x00);
+
+ // TTL
+ bb.put((byte) 64);
+
+ // Protocol
+ bb.put(proto);
+
+ // Header Checksum
+ final int ipChecksumOffset = bb.position();
+ bb.putShort((short) 0);
+
+ // Src/Dst addresses
+ bb.put(srcAddr.getAddress());
+ bb.put(dstAddr.getAddress());
+
+ bb.putShort(ipChecksumOffset, calculateChecksum(bb));
+
+ return bb;
+ }
+
+ private short calculateChecksum(ByteBuffer bb) {
+ int checksum = 0;
+
+ // Calculate sum of 16-bit values, excluding checksum. IPv4 headers are always 32-bit
+ // aligned, so no special cases needed for unaligned values.
+ ShortBuffer shortBuffer = ByteBuffer.wrap(getByteArrayFromBuffer(bb)).asShortBuffer();
+ while (shortBuffer.hasRemaining()) {
+ short val = shortBuffer.get();
+
+ // Wrap as needed
+ checksum = addAndWrapForChecksum(checksum, val);
+ }
+
+ return onesComplement(checksum);
+ }
+
+ public int getProtocolId() {
+ return IPPROTO_IPV4;
+ }
+ }
+
+ public static class Ip6Header extends IpHeader {
+ public Ip6Header(int nextHeader, Inet6Address src, Inet6Address dst, Payload payload) {
+ super(nextHeader, src, dst, payload);
+ }
+
+ public byte[] getPacketBytes() throws Exception {
+ ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+ // Version | Traffic Class (First 4 bits)
+ bb.put((byte) 0x60);
+
+ // Traffic class (Last 4 bits), Flow Label
+ bb.put((byte) 0);
+ bb.put((byte) 0);
+ bb.put((byte) 0);
+
+ // Payload Length
+ bb.putShort((short) payload.length());
+
+ // Next Header
+ bb.put(proto);
+
+ // Hop Limit
+ bb.put((byte) 64);
+
+ // Src/Dst addresses
+ bb.put(srcAddr.getAddress());
+ bb.put(dstAddr.getAddress());
+
+ // Payload
+ payload.addPacketBytes(this, bb);
+
+ return getByteArrayFromBuffer(bb);
+ }
+
+ public int getProtocolId() {
+ return IPPROTO_IPV6;
+ }
+ }
+
+ public static class BytePayload implements Payload {
+ public final byte[] payload;
+
+ public BytePayload(byte[] payload) {
+ this.payload = payload;
+ }
+
+ public int getProtocolId() {
+ return -1;
+ }
+
+ public byte[] getPacketBytes(IpHeader header) {
+ ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+ addPacketBytes(header, bb);
+ return getByteArrayFromBuffer(bb);
+ }
+
+ public void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) {
+ resultBuffer.put(payload);
+ }
+
+ public short length() {
+ return (short) payload.length;
+ }
+ }
+
+ public static class UdpHeader implements Payload {
+
+ public final short srcPort;
+ public final short dstPort;
+ public final Payload payload;
+
+ public UdpHeader(int srcPort, int dstPort, Payload payload) {
+ this.srcPort = (short) srcPort;
+ this.dstPort = (short) dstPort;
+ this.payload = payload;
+ }
+
+ public int getProtocolId() {
+ return IPPROTO_UDP;
+ }
+
+ public short length() {
+ return (short) (payload.length() + 8);
+ }
+
+ public byte[] getPacketBytes(IpHeader header) throws Exception {
+ ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+ addPacketBytes(header, bb);
+ return getByteArrayFromBuffer(bb);
+ }
+
+ public void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) throws Exception {
+ // Source, Destination port
+ resultBuffer.putShort(srcPort);
+ resultBuffer.putShort(dstPort);
+
+ // Payload Length
+ resultBuffer.putShort(length());
+
+ // Get payload bytes for checksum + payload
+ ByteBuffer payloadBuffer = ByteBuffer.allocate(DATA_BUFFER_LEN);
+ payload.addPacketBytes(header, payloadBuffer);
+ byte[] payloadBytes = getByteArrayFromBuffer(payloadBuffer);
+
+ // Checksum
+ resultBuffer.putShort(calculateChecksum(header, payloadBytes));
+
+ // Payload
+ resultBuffer.put(payloadBytes);
+ }
+
+ private short calculateChecksum(IpHeader header, byte[] payloadBytes) throws Exception {
+ int newChecksum = 0;
+ ShortBuffer srcBuffer = ByteBuffer.wrap(header.srcAddr.getAddress()).asShortBuffer();
+ ShortBuffer dstBuffer = ByteBuffer.wrap(header.dstAddr.getAddress()).asShortBuffer();
+
+ while (srcBuffer.hasRemaining() || dstBuffer.hasRemaining()) {
+ short val = srcBuffer.hasRemaining() ? srcBuffer.get() : dstBuffer.get();
+
+ // Wrap as needed
+ newChecksum = addAndWrapForChecksum(newChecksum, val);
+ }
+
+ // Add pseudo-header values. Proto is 0-padded, so just use the byte.
+ newChecksum = addAndWrapForChecksum(newChecksum, header.proto);
+ newChecksum = addAndWrapForChecksum(newChecksum, length());
+ newChecksum = addAndWrapForChecksum(newChecksum, srcPort);
+ newChecksum = addAndWrapForChecksum(newChecksum, dstPort);
+ newChecksum = addAndWrapForChecksum(newChecksum, length());
+
+ ShortBuffer payloadShortBuffer = ByteBuffer.wrap(payloadBytes).asShortBuffer();
+ while (payloadShortBuffer.hasRemaining()) {
+ newChecksum = addAndWrapForChecksum(newChecksum, payloadShortBuffer.get());
+ }
+ if (payload.length() % 2 != 0) {
+ newChecksum =
+ addAndWrapForChecksum(
+ newChecksum, (payloadBytes[payloadBytes.length - 1] << 8));
+ }
+
+ return onesComplement(newChecksum);
+ }
+ }
+
+ public static class EspHeader implements Payload {
+ public final int nextHeader;
+ public final int spi;
+ public final int seqNum;
+ public final byte[] key;
+ public final byte[] payload;
+
+ /**
+ * Generic constructor for ESP headers.
+ *
+ * <p>For Tunnel mode, payload will be a full IP header + attached payloads
+ *
+ * <p>For Transport mode, payload will be only the attached payloads, but with the checksum
+ * calculated using the pre-encryption IP header
+ */
+ public EspHeader(int nextHeader, int spi, int seqNum, byte[] key, byte[] payload) {
+ this.nextHeader = nextHeader;
+ this.spi = spi;
+ this.seqNum = seqNum;
+ this.key = key;
+ this.payload = payload;
+ }
+
+ public int getProtocolId() {
+ return IPPROTO_ESP;
+ }
+
+ public short length() {
+ // ALWAYS uses AES-CBC, HMAC-SHA256 (128b trunc len)
+ return (short)
+ calculateEspPacketSize(payload.length, AES_CBC_IV_LEN, AES_CBC_BLK_SIZE, 128);
+ }
+
+ public byte[] getPacketBytes(IpHeader header) throws Exception {
+ ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+ addPacketBytes(header, bb);
+ return getByteArrayFromBuffer(bb);
+ }
+
+ public void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) throws Exception {
+ ByteBuffer espPayloadBuffer = ByteBuffer.allocate(DATA_BUFFER_LEN);
+ espPayloadBuffer.putInt(spi);
+ espPayloadBuffer.putInt(seqNum);
+ espPayloadBuffer.put(getCiphertext(key));
+
+ espPayloadBuffer.put(getIcv(getByteArrayFromBuffer(espPayloadBuffer)), 0, 16);
+ resultBuffer.put(getByteArrayFromBuffer(espPayloadBuffer));
+ }
+
+ private byte[] getIcv(byte[] authenticatedSection) throws GeneralSecurityException {
+ Mac sha256HMAC = Mac.getInstance(HMAC_SHA_256);
+ SecretKeySpec authKey = new SecretKeySpec(key, HMAC_SHA_256);
+ sha256HMAC.init(authKey);
+
+ return sha256HMAC.doFinal(authenticatedSection);
+ }
+
+ /**
+ * Encrypts and builds ciphertext block. Includes the IV, Padding and Next-Header blocks
+ *
+ * <p>The ciphertext does NOT include the SPI/Sequence numbers, or the ICV.
+ */
+ private byte[] getCiphertext(byte[] key) throws GeneralSecurityException {
+ int paddedLen = calculateEspEncryptedLength(payload.length, AES_CBC_BLK_SIZE);
+ ByteBuffer paddedPayload = ByteBuffer.allocate(paddedLen);
+ paddedPayload.put(payload);
+
+ // Add padding - consecutive integers from 0x01
+ int pad = 1;
+ while (paddedPayload.position() < paddedPayload.limit()) {
+ paddedPayload.put((byte) pad++);
+ }
+
+ paddedPayload.position(paddedPayload.limit() - 2);
+ paddedPayload.put((byte) (paddedLen - 2 - payload.length)); // Pad length
+ paddedPayload.put((byte) nextHeader);
+
+ // Generate Initialization Vector
+ byte[] iv = new byte[AES_CBC_IV_LEN];
+ new SecureRandom().nextBytes(iv);
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
+ SecretKeySpec secretKeySpec = new SecretKeySpec(key, AES);
+
+ // Encrypt payload
+ Cipher cipher = Cipher.getInstance(AES_CBC);
+ cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
+ byte[] encrypted = cipher.doFinal(getByteArrayFromBuffer(paddedPayload));
+
+ // Build ciphertext
+ ByteBuffer cipherText = ByteBuffer.allocate(AES_CBC_IV_LEN + encrypted.length);
+ cipherText.put(iv);
+ cipherText.put(encrypted);
+
+ return getByteArrayFromBuffer(cipherText);
+ }
+ }
+
+ private static int addAndWrapForChecksum(int currentChecksum, int value) {
+ currentChecksum += value & 0x0000ffff;
+
+ // Wrap anything beyond the first 16 bits, and add to lower order bits
+ return (currentChecksum >>> 16) + (currentChecksum & 0x0000ffff);
+ }
+
+ private static short onesComplement(int val) {
+ val = (val >>> 16) + (val & 0xffff);
+
+ if (val == 0) return 0;
+ return (short) ((~val) & 0xffff);
+ }
+
+ public static int calculateEspPacketSize(
+ int payloadLen, int cryptIvLength, int cryptBlockSize, int authTruncLen) {
+ final int ESP_HDRLEN = 4 + 4; // SPI + Seq#
+ final int ICV_LEN = authTruncLen / 8; // Auth trailer; based on truncation length
+ payloadLen += cryptIvLength; // Initialization Vector
+
+ // Align to block size of encryption algorithm
+ payloadLen = calculateEspEncryptedLength(payloadLen, cryptBlockSize);
+ return payloadLen + ESP_HDRLEN + ICV_LEN;
+ }
+
+ private static int calculateEspEncryptedLength(int payloadLen, int cryptBlockSize) {
+ payloadLen += 2; // ESP trailer
+
+ // Align to block size of encryption algorithm
+ return payloadLen + calculateEspPadLen(payloadLen, cryptBlockSize);
+ }
+
+ private static int calculateEspPadLen(int payloadLen, int cryptBlockSize) {
+ return (cryptBlockSize - (payloadLen % cryptBlockSize)) % cryptBlockSize;
+ }
+
+ private static byte[] getByteArrayFromBuffer(ByteBuffer buffer) {
+ return Arrays.copyOfRange(buffer.array(), 0, buffer.position());
+ }
+
+ public static IpHeader getIpHeader(
+ int protocol, InetAddress src, InetAddress dst, Payload payload) {
+ if ((src instanceof Inet6Address) != (dst instanceof Inet6Address)) {
+ throw new IllegalArgumentException("Invalid src/dst address combination");
+ }
+
+ if (src instanceof Inet6Address) {
+ return new Ip6Header(protocol, (Inet6Address) src, (Inet6Address) dst, payload);
+ } else {
+ return new Ip4Header(protocol, (Inet4Address) src, (Inet4Address) dst, payload);
+ }
+ }
+
+ /*
+ * Debug printing
+ */
+ private static final char[] hexArray = "0123456789ABCDEF".toCharArray();
+
+ public static String bytesToHex(byte[] bytes) {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ sb.append(hexArray[b >>> 4]);
+ sb.append(hexArray[b & 0x0F]);
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/ProxyInfoTest.java b/tests/cts/net/src/android/net/cts/ProxyInfoTest.java
new file mode 100644
index 0000000..1c5624c
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/ProxyInfoTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2019 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 static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.ProxyInfo;
+import android.net.Uri;
+import android.os.Build;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+@RunWith(AndroidJUnit4.class)
+public final class ProxyInfoTest {
+ private static final String TEST_HOST = "test.example.com";
+ private static final int TEST_PORT = 5566;
+ private static final Uri TEST_URI = Uri.parse("https://test.example.com");
+ // This matches android.net.ProxyInfo#LOCAL_EXCL_LIST
+ private static final String LOCAL_EXCL_LIST = "";
+ // This matches android.net.ProxyInfo#LOCAL_HOST
+ private static final String LOCAL_HOST = "localhost";
+ // This matches android.net.ProxyInfo#LOCAL_PORT
+ private static final int LOCAL_PORT = -1;
+
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+ @Test
+ public void testConstructor() {
+ final ProxyInfo proxy = new ProxyInfo((ProxyInfo) null);
+ checkEmpty(proxy);
+
+ assertEquals(proxy, new ProxyInfo(proxy));
+ }
+
+ @Test
+ public void testBuildDirectProxy() {
+ final ProxyInfo proxy1 = ProxyInfo.buildDirectProxy(TEST_HOST, TEST_PORT);
+
+ assertEquals(TEST_HOST, proxy1.getHost());
+ assertEquals(TEST_PORT, proxy1.getPort());
+ assertArrayEquals(new String[0], proxy1.getExclusionList());
+ assertEquals(Uri.EMPTY, proxy1.getPacFileUrl());
+
+ final List<String> exclList = new ArrayList<>();
+ exclList.add("localhost");
+ exclList.add("*.exclusion.com");
+ final ProxyInfo proxy2 = ProxyInfo.buildDirectProxy(TEST_HOST, TEST_PORT, exclList);
+
+ assertEquals(TEST_HOST, proxy2.getHost());
+ assertEquals(TEST_PORT, proxy2.getPort());
+ assertArrayEquals(exclList.toArray(new String[0]), proxy2.getExclusionList());
+ assertEquals(Uri.EMPTY, proxy2.getPacFileUrl());
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testBuildPacProxy() {
+ final ProxyInfo proxy1 = ProxyInfo.buildPacProxy(TEST_URI);
+
+ assertEquals(LOCAL_HOST, proxy1.getHost());
+ assertEquals(LOCAL_PORT, proxy1.getPort());
+ assertArrayEquals(LOCAL_EXCL_LIST.toLowerCase(Locale.ROOT).split(","),
+ proxy1.getExclusionList());
+ assertEquals(TEST_URI, proxy1.getPacFileUrl());
+
+ final ProxyInfo proxy2 = ProxyInfo.buildPacProxy(TEST_URI, TEST_PORT);
+
+ assertEquals(LOCAL_HOST, proxy2.getHost());
+ assertEquals(TEST_PORT, proxy2.getPort());
+ assertArrayEquals(LOCAL_EXCL_LIST.toLowerCase(Locale.ROOT).split(","),
+ proxy2.getExclusionList());
+ assertEquals(TEST_URI, proxy2.getPacFileUrl());
+ }
+
+ @Test
+ public void testIsValid() {
+ final ProxyInfo proxy1 = ProxyInfo.buildDirectProxy(TEST_HOST, TEST_PORT);
+ assertTrue(proxy1.isValid());
+
+ // Given empty host
+ final ProxyInfo proxy2 = ProxyInfo.buildDirectProxy("", TEST_PORT);
+ assertFalse(proxy2.isValid());
+ // Given invalid host
+ final ProxyInfo proxy3 = ProxyInfo.buildDirectProxy(".invalid.com", TEST_PORT);
+ assertFalse(proxy3.isValid());
+ // Given invalid port.
+ final ProxyInfo proxy4 = ProxyInfo.buildDirectProxy(TEST_HOST, 0);
+ assertFalse(proxy4.isValid());
+ // Given another invalid port
+ final ProxyInfo proxy5 = ProxyInfo.buildDirectProxy(TEST_HOST, 65536);
+ assertFalse(proxy5.isValid());
+ // Given invalid exclusion list
+ final List<String> exclList = new ArrayList<>();
+ exclList.add(".invalid.com");
+ exclList.add("%.test.net");
+ final ProxyInfo proxy6 = ProxyInfo.buildDirectProxy(TEST_HOST, TEST_PORT, exclList);
+ assertFalse(proxy6.isValid());
+ }
+
+ private void checkEmpty(ProxyInfo proxy) {
+ assertNull(proxy.getHost());
+ assertEquals(0, proxy.getPort());
+ assertNull(proxy.getExclusionList());
+ assertEquals(Uri.EMPTY, proxy.getPacFileUrl());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/ProxyTest.java b/tests/cts/net/src/android/net/cts/ProxyTest.java
new file mode 100644
index 0000000..467d12f
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/ProxyTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2009 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.Proxy;
+import android.test.AndroidTestCase;
+
+public class ProxyTest extends AndroidTestCase {
+
+ public void testConstructor() {
+ new Proxy();
+ }
+
+ public void testAccessProperties() {
+ final int minValidPort = 0;
+ final int maxValidPort = 65535;
+ int defaultPort = Proxy.getDefaultPort();
+ if(null == Proxy.getDefaultHost()) {
+ assertEquals(-1, defaultPort);
+ } else {
+ assertTrue(defaultPort >= minValidPort && defaultPort <= maxValidPort);
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/RssiCurveTest.java b/tests/cts/net/src/android/net/cts/RssiCurveTest.java
new file mode 100644
index 0000000..d651b71
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/RssiCurveTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2020 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 static com.google.common.truth.Truth.assertThat;
+
+import android.net.RssiCurve;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** CTS tests for {@link RssiCurve}. */
+@RunWith(AndroidJUnit4.class)
+public class RssiCurveTest {
+
+ @Test
+ public void lookupScore_constantCurve() {
+ // One bucket from rssi=-100 to 100 with score 10.
+ RssiCurve curve = new RssiCurve(-100, 200, new byte[] { 10 });
+ assertThat(curve.lookupScore(-200)).isEqualTo(10);
+ assertThat(curve.lookupScore(-100)).isEqualTo(10);
+ assertThat(curve.lookupScore(0)).isEqualTo(10);
+ assertThat(curve.lookupScore(100)).isEqualTo(10);
+ assertThat(curve.lookupScore(200)).isEqualTo(10);
+ }
+
+ @Test
+ public void lookupScore_changingCurve() {
+ // One bucket from -100 to 0 with score -10, and one bucket from 0 to 100 with score 10.
+ RssiCurve curve = new RssiCurve(-100, 100, new byte[] { -10, 10 });
+ assertThat(curve.lookupScore(-200)).isEqualTo(-10);
+ assertThat(curve.lookupScore(-100)).isEqualTo(-10);
+ assertThat(curve.lookupScore(-50)).isEqualTo(-10);
+ assertThat(curve.lookupScore(0)).isEqualTo(10);
+ assertThat(curve.lookupScore(50)).isEqualTo(10);
+ assertThat(curve.lookupScore(100)).isEqualTo(10);
+ assertThat(curve.lookupScore(200)).isEqualTo(10);
+ }
+
+ @Test
+ public void lookupScore_linearCurve() {
+ // Curve starting at -110, with 15 buckets of width 10 whose scores increases by 10 with
+ // each bucket. The current active network gets a boost of 15 to its RSSI.
+ RssiCurve curve = new RssiCurve(
+ -110,
+ 10,
+ new byte[] { -20, -10, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120 },
+ 15);
+
+ assertThat(curve.lookupScore(-120)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-120, false)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-120, true)).isEqualTo(-20);
+
+ assertThat(curve.lookupScore(-111)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-111, false)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-111, true)).isEqualTo(-10);
+
+ assertThat(curve.lookupScore(-110)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-110, false)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-110, true)).isEqualTo(-10);
+
+ assertThat(curve.lookupScore(-105)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-105, false)).isEqualTo(-20);
+ assertThat(curve.lookupScore(-105, true)).isEqualTo(0);
+
+ assertThat(curve.lookupScore(-100)).isEqualTo(-10);
+ assertThat(curve.lookupScore(-100, false)).isEqualTo(-10);
+ assertThat(curve.lookupScore(-100, true)).isEqualTo(0);
+
+ assertThat(curve.lookupScore(-50)).isEqualTo(40);
+ assertThat(curve.lookupScore(-50, false)).isEqualTo(40);
+ assertThat(curve.lookupScore(-50, true)).isEqualTo(50);
+
+ assertThat(curve.lookupScore(0)).isEqualTo(90);
+ assertThat(curve.lookupScore(0, false)).isEqualTo(90);
+ assertThat(curve.lookupScore(0, true)).isEqualTo(100);
+
+ assertThat(curve.lookupScore(30)).isEqualTo(120);
+ assertThat(curve.lookupScore(30, false)).isEqualTo(120);
+ assertThat(curve.lookupScore(30, true)).isEqualTo(120);
+
+ assertThat(curve.lookupScore(40)).isEqualTo(120);
+ assertThat(curve.lookupScore(40, false)).isEqualTo(120);
+ assertThat(curve.lookupScore(40, true)).isEqualTo(120);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/SSLCertificateSocketFactoryTest.java b/tests/cts/net/src/android/net/cts/SSLCertificateSocketFactoryTest.java
new file mode 100644
index 0000000..cbe54f8
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/SSLCertificateSocketFactoryTest.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2008 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 static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.SSLCertificateSocketFactory;
+import android.platform.test.annotations.AppModeFull;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import libcore.javax.net.ssl.SSLConfigurationAsserts;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SSLCertificateSocketFactoryTest {
+ // TEST_HOST should point to a web server with a valid TLS certificate.
+ private static final String TEST_HOST = "www.google.com";
+ private static final int HTTPS_PORT = 443;
+ private HostnameVerifier mDefaultVerifier;
+ private SSLCertificateSocketFactory mSocketFactory;
+ private InetAddress mLocalAddress;
+ // InetAddress obtained by resolving TEST_HOST.
+ private InetAddress mTestHostAddress;
+ // SocketAddress combining mTestHostAddress and HTTPS_PORT.
+ private List<SocketAddress> mTestSocketAddresses;
+
+ @Before
+ public void setUp() {
+ // Expected state before each test method is that
+ // HttpsURLConnection.getDefaultHostnameVerifier() will return the system default.
+ mDefaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
+ mSocketFactory = (SSLCertificateSocketFactory)
+ SSLCertificateSocketFactory.getDefault(1000 /* handshakeTimeoutMillis */);
+ assertNotNull(mSocketFactory);
+ InetAddress[] addresses;
+ try {
+ addresses = InetAddress.getAllByName(TEST_HOST);
+ mTestHostAddress = addresses[0];
+ } catch (UnknownHostException uhe) {
+ throw new AssertionError(
+ "Unable to test SSLCertificateSocketFactory: cannot resolve " + TEST_HOST, uhe);
+ }
+
+ mTestSocketAddresses = Arrays.stream(addresses)
+ .map(addr -> new InetSocketAddress(addr, HTTPS_PORT))
+ .collect(Collectors.toList());
+
+ // Find the local IP address which will be used to connect to TEST_HOST.
+ try {
+ Socket testSocket = new Socket(TEST_HOST, HTTPS_PORT);
+ mLocalAddress = testSocket.getLocalAddress();
+ testSocket.close();
+ } catch (IOException ioe) {
+ throw new AssertionError(""
+ + "Unable to test SSLCertificateSocketFactory: cannot connect to "
+ + TEST_HOST, ioe);
+ }
+ }
+
+ // Restore the system default hostname verifier after each test.
+ @After
+ public void restoreDefaultHostnameVerifier() {
+ HttpsURLConnection.setDefaultHostnameVerifier(mDefaultVerifier);
+ }
+
+ @Test
+ public void testDefaultConfiguration() throws Exception {
+ SSLConfigurationAsserts.assertSSLSocketFactoryDefaultConfiguration(mSocketFactory);
+ }
+
+ @Test
+ public void testAccessProperties() {
+ mSocketFactory.getSupportedCipherSuites();
+ mSocketFactory.getDefaultCipherSuites();
+ }
+
+ /**
+ * Tests the {@code createSocket()} cases which are expected to fail with {@code IOException}.
+ */
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void createSocket_io_error_expected() {
+ // Connect to the localhost HTTPS port. Should result in connection refused IOException
+ // because no service should be listening on that port.
+ InetAddress localhostAddress = InetAddress.getLoopbackAddress();
+ try {
+ mSocketFactory.createSocket(localhostAddress, HTTPS_PORT);
+ fail();
+ } catch (IOException e) {
+ // expected
+ }
+
+ // Same, but also binding to a local address.
+ try {
+ mSocketFactory.createSocket(localhostAddress, HTTPS_PORT, localhostAddress, 0);
+ fail();
+ } catch (IOException e) {
+ // expected
+ }
+
+ // Same, wrapping an existing plain socket which is in an unconnected state.
+ try {
+ Socket socket = new Socket();
+ mSocketFactory.createSocket(socket, "localhost", HTTPS_PORT, true);
+ fail();
+ } catch (IOException e) {
+ // expected
+ }
+ }
+
+ /**
+ * Tests hostname verification for
+ * {@link SSLCertificateSocketFactory#createSocket(String, int)}.
+ *
+ * <p>This method should return a socket which is fully connected (i.e. TLS handshake complete)
+ * and whose peer TLS certificate has been verified to have the correct hostname.
+ *
+ * <p>{@link SSLCertificateSocketFactory} is documented to verify hostnames using
+ * the {@link HostnameVerifier} returned by
+ * {@link HttpsURLConnection#getDefaultHostnameVerifier}, so this test connects twice,
+ * once with the system default {@link HostnameVerifier} which is expected to succeed,
+ * and once after installing a {@link NegativeHostnameVerifier} which will cause
+ * {@link SSLCertificateSocketFactory#verifyHostname} to throw a
+ * {@link SSLPeerUnverifiedException}.
+ *
+ * <p>These tests only test the hostname verification logic in SSLCertificateSocketFactory,
+ * other TLS failure modes and the default HostnameVerifier are tested elsewhere, see
+ * {@link com.squareup.okhttp.internal.tls.HostnameVerifierTest} and
+ * https://android.googlesource.com/platform/external/boringssl/+/refs/heads/master/src/ssl/test
+ *
+ * <p>Tests the following behaviour:-
+ * <ul>
+ * <li>TEST_SERVER is available and has a valid TLS certificate
+ * <li>{@code createSocket()} verifies the remote hostname is correct using
+ * {@link HttpsURLConnection#getDefaultHostnameVerifier}
+ * <li>{@link SSLPeerUnverifiedException} is thrown when the remote hostname is invalid
+ * </ul>
+ *
+ * <p>See also http://b/2807618.
+ */
+ @Test
+ public void createSocket_simple_with_hostname_verification() throws Exception {
+ Socket socket = mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT);
+ assertConnectedSocket(socket);
+ socket.close();
+
+ HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
+ try {
+ mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT);
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ // expected
+ }
+ }
+
+ /**
+ * Tests hostname verification for
+ * {@link SSLCertificateSocketFactory#createSocket(Socket, String, int, boolean)}.
+ *
+ * <p>This method should return a socket which is fully connected (i.e. TLS handshake complete)
+ * and whose peer TLS certificate has been verified to have the correct hostname.
+ *
+ * <p>The TLS socket returned is wrapped around the plain socket passed into
+ * {@code createSocket()}.
+ *
+ * <p>See {@link #createSocket_simple_with_hostname_verification()} for test methodology.
+ */
+ @Test
+ public void createSocket_wrapped_with_hostname_verification() throws Exception {
+ Socket underlying = new Socket(TEST_HOST, HTTPS_PORT);
+ Socket socket = mSocketFactory.createSocket(underlying, TEST_HOST, HTTPS_PORT, true);
+ assertConnectedSocket(socket);
+ socket.close();
+
+ HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
+ try {
+ underlying = new Socket(TEST_HOST, HTTPS_PORT);
+ mSocketFactory.createSocket(underlying, TEST_HOST, HTTPS_PORT, true);
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ // expected
+ }
+ }
+
+ /**
+ * Tests hostname verification for
+ * {@link SSLCertificateSocketFactory#createSocket(String, int, InetAddress, int)}.
+ *
+ * <p>This method should return a socket which is fully connected (i.e. TLS handshake complete)
+ * and whose peer TLS certificate has been verified to have the correct hostname.
+ *
+ * <p>The TLS socket returned is also bound to the local address determined in {@link #setUp} to
+ * be used for connections to TEST_HOST, and a wildcard port.
+ *
+ * <p>See {@link #createSocket_simple_with_hostname_verification()} for test methodology.
+ */
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void createSocket_bound_with_hostname_verification() throws Exception {
+ Socket socket = mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT, mLocalAddress, 0);
+ assertConnectedSocket(socket);
+ socket.close();
+
+ HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
+ try {
+ mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT, mLocalAddress, 0);
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ // expected
+ }
+ }
+
+ /**
+ * Tests hostname verification for
+ * {@link SSLCertificateSocketFactory#createSocket(InetAddress, int)}.
+ *
+ * <p>This method should return a socket which the documentation describes as "unconnected",
+ * which actually means that the socket is fully connected at the TCP layer but TLS handshaking
+ * and hostname verification have not yet taken place.
+ *
+ * <p>Behaviour is tested by installing a {@link NegativeHostnameVerifier} and by calling
+ * {@link #assertConnectedSocket} to ensure TLS handshaking but no hostname verification takes
+ * place. Next, {@link SSLCertificateSocketFactory#verifyHostname} is called to ensure
+ * that hostname verification is using the {@link HostnameVerifier} returned by
+ * {@link HttpsURLConnection#getDefaultHostnameVerifier} as documented.
+ *
+ * <p>Tests the following behaviour:-
+ * <ul>
+ * <li>TEST_SERVER is available and has a valid TLS certificate
+ * <li>{@code createSocket()} does not verify the remote hostname
+ * <li>Calling {@link SSLCertificateSocketFactory#verifyHostname} on the returned socket
+ * throws {@link SSLPeerUnverifiedException} if the remote hostname is invalid
+ * </ul>
+ */
+ @Test
+ public void createSocket_simple_no_hostname_verification() throws Exception{
+ HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
+ Socket socket = mSocketFactory.createSocket(mTestHostAddress, HTTPS_PORT);
+ // Need to provide the expected hostname here or the TLS handshake will
+ // be unable to supply SNI to the remote host.
+ mSocketFactory.setHostname(socket, TEST_HOST);
+ assertConnectedSocket(socket);
+ try {
+ SSLCertificateSocketFactory.verifyHostname(socket, TEST_HOST);
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ // expected
+ }
+ HttpsURLConnection.setDefaultHostnameVerifier(mDefaultVerifier);
+ SSLCertificateSocketFactory.verifyHostname(socket, TEST_HOST);
+ socket.close();
+ }
+
+ /**
+ * Tests hostname verification for
+ * {@link SSLCertificateSocketFactory#createSocket(InetAddress, int, InetAddress, int)}.
+ *
+ * <p>This method should return a socket which the documentation describes as "unconnected",
+ * which actually means that the socket is fully connected at the TCP layer but TLS handshaking
+ * and hostname verification have not yet taken place.
+ *
+ * <p>The TLS socket returned is also bound to the local address determined in {@link #setUp} to
+ * be used for connections to TEST_HOST, and a wildcard port.
+ *
+ * <p>See {@link #createSocket_simple_no_hostname_verification()} for test methodology.
+ */
+ @Test
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void createSocket_bound_no_hostname_verification() throws Exception{
+ HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
+ Socket socket =
+ mSocketFactory.createSocket(mTestHostAddress, HTTPS_PORT, mLocalAddress, 0);
+ // Need to provide the expected hostname here or the TLS handshake will
+ // be unable to supply SNI to the peer.
+ mSocketFactory.setHostname(socket, TEST_HOST);
+ assertConnectedSocket(socket);
+ try {
+ SSLCertificateSocketFactory.verifyHostname(socket, TEST_HOST);
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ // expected
+ }
+ HttpsURLConnection.setDefaultHostnameVerifier(mDefaultVerifier);
+ SSLCertificateSocketFactory.verifyHostname(socket, TEST_HOST);
+ socket.close();
+ }
+
+ /**
+ * Asserts a socket is fully connected to the expected peer.
+ *
+ * <p>For the variants of createSocket which verify the remote hostname,
+ * {@code socket} should already be fully connected.
+ *
+ * <p>For the non-verifying variants, retrieving the input stream will trigger a TLS handshake
+ * and so may throw an exception, for example if the peer's certificate is invalid.
+ *
+ * <p>Does no hostname verification.
+ */
+ private void assertConnectedSocket(Socket socket) throws Exception {
+ assertNotNull(socket);
+ assertTrue(socket.isConnected());
+ assertNotNull(socket.getInputStream());
+ assertNotNull(socket.getOutputStream());
+ assertTrue(mTestSocketAddresses.contains(socket.getRemoteSocketAddress()));
+ }
+
+ /**
+ * A HostnameVerifier which always returns false to simulate a server returning a
+ * certificate which does not match the expected hostname.
+ */
+ private static class NegativeHostnameVerifier implements HostnameVerifier {
+ @Override
+ public boolean verify(String hostname, SSLSession sslSession) {
+ return false;
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/TheaterModeTest.java b/tests/cts/net/src/android/net/cts/TheaterModeTest.java
new file mode 100644
index 0000000..d1ddeaa
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/TheaterModeTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2016 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.ContentResolver;
+import android.content.Context;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+public class TheaterModeTest extends AndroidTestCase {
+ private static final String TAG = "TheaterModeTest";
+ private static final String FEATURE_BLUETOOTH = "android.hardware.bluetooth";
+ private static final String FEATURE_WIFI = "android.hardware.wifi";
+ private static final int TIMEOUT_MS = 10 * 1000;
+ private boolean mHasFeature;
+ private Context mContext;
+ private ContentResolver resolver;
+
+ public void setup() {
+ mContext= getContext();
+ resolver = mContext.getContentResolver();
+ mHasFeature = (mContext.getPackageManager().hasSystemFeature(FEATURE_BLUETOOTH)
+ || mContext.getPackageManager().hasSystemFeature(FEATURE_WIFI));
+ }
+
+ @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
+ public void testTheaterMode() {
+ setup();
+ if (!mHasFeature) {
+ Log.i(TAG, "The device doesn't support network bluetooth or wifi feature");
+ return;
+ }
+
+ for (int testCount = 0; testCount < 2; testCount++) {
+ if (!doOneTest()) {
+ fail("Theater mode failed to change in " + TIMEOUT_MS + "msec");
+ return;
+ }
+ }
+ }
+
+ private boolean doOneTest() {
+ boolean theaterModeOn = isTheaterModeOn();
+
+ setTheaterModeOn(!theaterModeOn);
+ try {
+ Thread.sleep(TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Sleep time interrupted.", e);
+ }
+
+ if (theaterModeOn == isTheaterModeOn()) {
+ return false;
+ }
+ return true;
+ }
+
+ private void setTheaterModeOn(boolean enabling) {
+ // Change the system setting for theater mode
+ Settings.Global.putInt(resolver, Settings.Global.THEATER_MODE_ON, enabling ? 1 : 0);
+ }
+
+ private boolean isTheaterModeOn() {
+ // Read the system setting for theater mode
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.THEATER_MODE_ON, 0) != 0;
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/TrafficStatsTest.java b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
new file mode 100755
index 0000000..37bdd44
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2010 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.NetworkStats;
+import android.net.TrafficStats;
+import android.os.Process;
+import android.platform.test.annotations.AppModeFull;
+import android.test.AndroidTestCase;
+import android.util.Log;
+import android.util.Range;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class TrafficStatsTest extends AndroidTestCase {
+ private static final String LOG_TAG = "TrafficStatsTest";
+
+ /** Verify the given value is in range [lower, upper] */
+ private void assertInRange(String tag, long value, long lower, long upper) {
+ final Range range = new Range(lower, upper);
+ assertTrue(tag + ": " + value + " is not within range [" + lower + ", " + upper + "]",
+ range.contains(value));
+ }
+
+ public void testValidMobileStats() {
+ // We can't assume a mobile network is even present in this test, so
+ // we simply assert that a valid value is returned.
+
+ assertTrue(TrafficStats.getMobileTxPackets() >= 0);
+ assertTrue(TrafficStats.getMobileRxPackets() >= 0);
+ assertTrue(TrafficStats.getMobileTxBytes() >= 0);
+ assertTrue(TrafficStats.getMobileRxBytes() >= 0);
+ }
+
+ public void testValidTotalStats() {
+ assertTrue(TrafficStats.getTotalTxPackets() >= 0);
+ assertTrue(TrafficStats.getTotalRxPackets() >= 0);
+ assertTrue(TrafficStats.getTotalTxBytes() >= 0);
+ assertTrue(TrafficStats.getTotalRxBytes() >= 0);
+ }
+
+ public void testValidPacketStats() {
+ assertTrue(TrafficStats.getTxPackets("lo") >= 0);
+ assertTrue(TrafficStats.getRxPackets("lo") >= 0);
+ }
+
+ public void testThreadStatsTag() throws Exception {
+ TrafficStats.setThreadStatsTag(0xf00d);
+ assertTrue("Tag didn't stick", TrafficStats.getThreadStatsTag() == 0xf00d);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ new Thread("TrafficStatsTest.testThreadStatsTag") {
+ @Override
+ public void run() {
+ assertTrue("Tag leaked", TrafficStats.getThreadStatsTag() != 0xf00d);
+ TrafficStats.setThreadStatsTag(0xcafe);
+ assertTrue("Tag didn't stick", TrafficStats.getThreadStatsTag() == 0xcafe);
+ latch.countDown();
+ }
+ }.start();
+
+ latch.await(5, TimeUnit.SECONDS);
+ assertTrue("Tag lost", TrafficStats.getThreadStatsTag() == 0xf00d);
+
+ TrafficStats.clearThreadStatsTag();
+ assertTrue("Tag not cleared", TrafficStats.getThreadStatsTag() != 0xf00d);
+ }
+
+ long tcpPacketToIpBytes(long packetCount, long bytes) {
+ // ip header + tcp header + data.
+ // Tcp header is mostly 32. Syn has different tcp options -> 40. Don't care.
+ return packetCount * (20 + 32 + bytes);
+ }
+
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testTrafficStatsForLocalhost() throws IOException {
+ final long mobileTxPacketsBefore = TrafficStats.getMobileTxPackets();
+ final long mobileRxPacketsBefore = TrafficStats.getMobileRxPackets();
+ final long mobileTxBytesBefore = TrafficStats.getMobileTxBytes();
+ final long mobileRxBytesBefore = TrafficStats.getMobileRxBytes();
+ final long totalTxPacketsBefore = TrafficStats.getTotalTxPackets();
+ final long totalRxPacketsBefore = TrafficStats.getTotalRxPackets();
+ final long totalTxBytesBefore = TrafficStats.getTotalTxBytes();
+ final long totalRxBytesBefore = TrafficStats.getTotalRxBytes();
+ final long uidTxBytesBefore = TrafficStats.getUidTxBytes(Process.myUid());
+ final long uidRxBytesBefore = TrafficStats.getUidRxBytes(Process.myUid());
+ final long uidTxPacketsBefore = TrafficStats.getUidTxPackets(Process.myUid());
+ final long uidRxPacketsBefore = TrafficStats.getUidRxPackets(Process.myUid());
+ final long ifaceTxPacketsBefore = TrafficStats.getTxPackets("lo");
+ final long ifaceRxPacketsBefore = TrafficStats.getRxPackets("lo");
+
+ // Transfer 1MB of data across an explicitly localhost socket.
+ final int byteCount = 1024;
+ final int packetCount = 1024;
+
+ TrafficStats.startDataProfiling(null);
+ final ServerSocket server = new ServerSocket(0);
+ new Thread("TrafficStatsTest.testTrafficStatsForLocalhost") {
+ @Override
+ public void run() {
+ try {
+ final Socket socket = new Socket("localhost", server.getLocalPort());
+ // Make sure that each write()+flush() turns into a packet:
+ // disable Nagle.
+ socket.setTcpNoDelay(true);
+ final OutputStream out = socket.getOutputStream();
+ final byte[] buf = new byte[byteCount];
+ TrafficStats.setThreadStatsTag(0x42);
+ TrafficStats.tagSocket(socket);
+ for (int i = 0; i < packetCount; i++) {
+ out.write(buf);
+ out.flush();
+ try {
+ // Bug: 10668088, Even with Nagle disabled, and flushing the 1024 bytes
+ // the kernel still regroups data into a larger packet.
+ Thread.sleep(5);
+ } catch (InterruptedException e) {
+ }
+ }
+ out.close();
+ socket.close();
+ } catch (IOException e) {
+ Log.i(LOG_TAG, "Badness during writes to socket: " + e);
+ }
+ }
+ }.start();
+
+ int read = 0;
+ try {
+ final Socket socket = server.accept();
+ socket.setTcpNoDelay(true);
+ TrafficStats.setThreadStatsTag(0x43);
+ TrafficStats.tagSocket(socket);
+ final InputStream in = socket.getInputStream();
+ final byte[] buf = new byte[byteCount];
+ while (read < byteCount * packetCount) {
+ int n = in.read(buf);
+ assertTrue("Unexpected EOF", n > 0);
+ read += n;
+ }
+ } finally {
+ server.close();
+ }
+ assertTrue("Not all data read back", read >= byteCount * packetCount);
+
+ // It's too fast to call getUidTxBytes function.
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ }
+ final NetworkStats testStats = TrafficStats.stopDataProfiling(null);
+
+ final long mobileTxPacketsAfter = TrafficStats.getMobileTxPackets();
+ final long mobileRxPacketsAfter = TrafficStats.getMobileRxPackets();
+ final long mobileTxBytesAfter = TrafficStats.getMobileTxBytes();
+ final long mobileRxBytesAfter = TrafficStats.getMobileRxBytes();
+ final long totalTxPacketsAfter = TrafficStats.getTotalTxPackets();
+ final long totalRxPacketsAfter = TrafficStats.getTotalRxPackets();
+ final long totalTxBytesAfter = TrafficStats.getTotalTxBytes();
+ final long totalRxBytesAfter = TrafficStats.getTotalRxBytes();
+ final long uidTxBytesAfter = TrafficStats.getUidTxBytes(Process.myUid());
+ final long uidRxBytesAfter = TrafficStats.getUidRxBytes(Process.myUid());
+ final long uidTxPacketsAfter = TrafficStats.getUidTxPackets(Process.myUid());
+ final long uidRxPacketsAfter = TrafficStats.getUidRxPackets(Process.myUid());
+ final long uidTxDeltaBytes = uidTxBytesAfter - uidTxBytesBefore;
+ final long uidTxDeltaPackets = uidTxPacketsAfter - uidTxPacketsBefore;
+ final long uidRxDeltaBytes = uidRxBytesAfter - uidRxBytesBefore;
+ final long uidRxDeltaPackets = uidRxPacketsAfter - uidRxPacketsBefore;
+ final long ifaceTxPacketsAfter = TrafficStats.getTxPackets("lo");
+ final long ifaceRxPacketsAfter = TrafficStats.getRxPackets("lo");
+ final long ifaceTxDeltaPackets = ifaceTxPacketsAfter - ifaceTxPacketsBefore;
+ final long ifaceRxDeltaPackets = ifaceRxPacketsAfter - ifaceRxPacketsBefore;
+
+ // Localhost traffic *does* count against per-UID stats.
+ /*
+ * Calculations:
+ * - bytes
+ * bytes is approx: packets * data + packets * acks;
+ * but sometimes there are less acks than packets, so we set a lower
+ * limit of 1 ack.
+ * - setup/teardown
+ * + 7 approx.: syn, syn-ack, ack, fin-ack, ack, fin-ack, ack;
+ * but sometimes the last find-acks just vanish, so we set a lower limit of +5.
+ */
+ final int maxExpectedExtraPackets = 7;
+ final int minExpectedExtraPackets = 5;
+
+ // Some other tests don't cleanup connections correctly.
+ // They have the same UID, so we discount their lingering traffic
+ // which happens only on non-localhost, such as TCP FIN retranmission packets
+ final long deltaTxOtherPackets = (totalTxPacketsAfter - totalTxPacketsBefore)
+ - uidTxDeltaPackets;
+ final long deltaRxOtherPackets = (totalRxPacketsAfter - totalRxPacketsBefore)
+ - uidRxDeltaPackets;
+ if (deltaTxOtherPackets > 0 || deltaRxOtherPackets > 0) {
+ Log.i(LOG_TAG, "lingering traffic data: " + deltaTxOtherPackets + "/"
+ + deltaRxOtherPackets);
+ }
+
+ // Check that the per-uid stats obtained from data profiling contain the expected values.
+ // The data profiling snapshot is generated from the readNetworkStatsDetail() method in
+ // networkStatsService, so it's possible to verify that the detailed stats for a given
+ // uid are correct.
+ final NetworkStats.Entry entry = testStats.getTotal(null, Process.myUid());
+ final long pktBytes = tcpPacketToIpBytes(packetCount, byteCount);
+ final long pktWithNoDataBytes = tcpPacketToIpBytes(packetCount, 0);
+ final long minExpExtraPktBytes = tcpPacketToIpBytes(minExpectedExtraPackets, 0);
+ final long maxExpExtraPktBytes = tcpPacketToIpBytes(maxExpectedExtraPackets, 0);
+ final long deltaTxOtherPktBytes = tcpPacketToIpBytes(deltaTxOtherPackets, 0);
+ final long deltaRxOtherPktBytes = tcpPacketToIpBytes(deltaRxOtherPackets, 0);
+ assertInRange("txPackets detail", entry.txPackets, packetCount + minExpectedExtraPackets,
+ uidTxDeltaPackets);
+ assertInRange("rxPackets detail", entry.rxPackets, packetCount + minExpectedExtraPackets,
+ uidRxDeltaPackets);
+ assertInRange("txBytes detail", entry.txBytes, pktBytes + minExpExtraPktBytes,
+ uidTxDeltaBytes);
+ assertInRange("rxBytes detail", entry.rxBytes, pktBytes + minExpExtraPktBytes,
+ uidRxDeltaBytes);
+ assertInRange("uidtxp", uidTxDeltaPackets, packetCount + minExpectedExtraPackets,
+ packetCount + packetCount + maxExpectedExtraPackets + deltaTxOtherPackets);
+ assertInRange("uidrxp", uidRxDeltaPackets, packetCount + minExpectedExtraPackets,
+ packetCount + packetCount + maxExpectedExtraPackets + deltaRxOtherPackets);
+ assertInRange("uidtxb", uidTxDeltaBytes, pktBytes + minExpExtraPktBytes,
+ pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaTxOtherPktBytes);
+ assertInRange("uidrxb", uidRxDeltaBytes, pktBytes + minExpExtraPktBytes,
+ pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaRxOtherPktBytes);
+ assertInRange("iftxp", ifaceTxDeltaPackets, packetCount + minExpectedExtraPackets,
+ packetCount + packetCount + maxExpectedExtraPackets + deltaTxOtherPackets);
+ assertInRange("ifrxp", ifaceRxDeltaPackets, packetCount + minExpectedExtraPackets,
+ packetCount + packetCount + maxExpectedExtraPackets + deltaRxOtherPackets);
+
+ // Localhost traffic *does* count against total stats.
+ // Check the total stats increased after test data transfer over localhost has been made.
+ assertTrue("ttxp: " + totalTxPacketsBefore + " -> " + totalTxPacketsAfter,
+ totalTxPacketsAfter >= totalTxPacketsBefore + uidTxDeltaPackets);
+ assertTrue("trxp: " + totalRxPacketsBefore + " -> " + totalRxPacketsAfter,
+ totalRxPacketsAfter >= totalRxPacketsBefore + uidRxDeltaPackets);
+ assertTrue("ttxb: " + totalTxBytesBefore + " -> " + totalTxBytesAfter,
+ totalTxBytesAfter >= totalTxBytesBefore + uidTxDeltaBytes);
+ assertTrue("trxb: " + totalRxBytesBefore + " -> " + totalRxBytesAfter,
+ totalRxBytesAfter >= totalRxBytesBefore + uidRxDeltaBytes);
+ assertTrue("iftxp: " + ifaceTxPacketsBefore + " -> " + ifaceTxPacketsAfter,
+ totalTxPacketsAfter >= totalTxPacketsBefore + ifaceTxDeltaPackets);
+ assertTrue("ifrxp: " + ifaceRxPacketsBefore + " -> " + ifaceRxPacketsAfter,
+ totalRxPacketsAfter >= totalRxPacketsBefore + ifaceRxDeltaPackets);
+
+ // Localhost traffic should *not* count against mobile stats,
+ // There might be some other traffic, but nowhere near 1MB.
+ assertInRange("mtxp", mobileTxPacketsAfter, mobileTxPacketsBefore,
+ mobileTxPacketsBefore + 500);
+ assertInRange("mrxp", mobileRxPacketsAfter, mobileRxPacketsBefore,
+ mobileRxPacketsBefore + 500);
+ assertInRange("mtxb", mobileTxBytesAfter, mobileTxBytesBefore,
+ mobileTxBytesBefore + 200000);
+ assertInRange("mrxb", mobileRxBytesAfter, mobileRxBytesBefore,
+ mobileRxBytesBefore + 200000);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/TunUtils.java b/tests/cts/net/src/android/net/cts/TunUtils.java
new file mode 100644
index 0000000..adaba9d
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/TunUtils.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2018 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 static android.net.cts.PacketUtils.IP4_HDRLEN;
+import static android.net.cts.PacketUtils.IP6_HDRLEN;
+import static android.net.cts.PacketUtils.IPPROTO_ESP;
+import static android.net.cts.PacketUtils.UDP_HDRLEN;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.os.ParcelFileDescriptor;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Predicate;
+
+public class TunUtils {
+ private static final String TAG = TunUtils.class.getSimpleName();
+
+ protected static final int IP4_ADDR_OFFSET = 12;
+ protected static final int IP4_ADDR_LEN = 4;
+ protected static final int IP6_ADDR_OFFSET = 8;
+ protected static final int IP6_ADDR_LEN = 16;
+ protected static final int IP4_PROTO_OFFSET = 9;
+ protected static final int IP6_PROTO_OFFSET = 6;
+
+ private static final int DATA_BUFFER_LEN = 4096;
+ private static final int TIMEOUT = 1000;
+
+ private final List<byte[]> mPackets = new ArrayList<>();
+ private final ParcelFileDescriptor mTunFd;
+ private final Thread mReaderThread;
+
+ public TunUtils(ParcelFileDescriptor tunFd) {
+ mTunFd = tunFd;
+
+ // Start background reader thread
+ mReaderThread =
+ new Thread(
+ () -> {
+ try {
+ // Loop will exit and thread will quit when tunFd is closed.
+ // Receiving either EOF or an exception will exit this reader loop.
+ // FileInputStream in uninterruptable, so there's no good way to
+ // ensure that this thread shuts down except upon FD closure.
+ while (true) {
+ byte[] intercepted = receiveFromTun();
+ if (intercepted == null) {
+ // Exit once we've hit EOF
+ return;
+ } else if (intercepted.length > 0) {
+ // Only save packet if we've received any bytes.
+ synchronized (mPackets) {
+ mPackets.add(intercepted);
+ mPackets.notifyAll();
+ }
+ }
+ }
+ } catch (IOException ignored) {
+ // Simply exit this reader thread
+ return;
+ }
+ });
+ mReaderThread.start();
+ }
+
+ private byte[] receiveFromTun() throws IOException {
+ FileInputStream in = new FileInputStream(mTunFd.getFileDescriptor());
+ byte[] inBytes = new byte[DATA_BUFFER_LEN];
+ int bytesRead = in.read(inBytes);
+
+ if (bytesRead < 0) {
+ return null; // return null for EOF
+ } else if (bytesRead >= DATA_BUFFER_LEN) {
+ throw new IllegalStateException("Too big packet. Fragmentation unsupported");
+ }
+ return Arrays.copyOf(inBytes, bytesRead);
+ }
+
+ private byte[] getFirstMatchingPacket(Predicate<byte[]> verifier, int startIndex) {
+ synchronized (mPackets) {
+ for (int i = startIndex; i < mPackets.size(); i++) {
+ byte[] pkt = mPackets.get(i);
+ if (verifier.test(pkt)) {
+ return pkt;
+ }
+ }
+ }
+ return null;
+ }
+
+ protected byte[] awaitPacket(Predicate<byte[]> verifier) throws Exception {
+ long endTime = System.currentTimeMillis() + TIMEOUT;
+ int startIndex = 0;
+
+ synchronized (mPackets) {
+ while (System.currentTimeMillis() < endTime) {
+ final byte[] pkt = getFirstMatchingPacket(verifier, startIndex);
+ if (pkt != null) {
+ return pkt; // We've found the packet we're looking for.
+ }
+
+ startIndex = mPackets.size();
+
+ // Try to prevent waiting too long. If waitTimeout <= 0, we've already hit timeout
+ long waitTimeout = endTime - System.currentTimeMillis();
+ if (waitTimeout > 0) {
+ mPackets.wait(waitTimeout);
+ }
+ }
+ }
+
+ fail("No packet found matching verifier");
+ throw new IllegalStateException("Impossible condition; should have thrown in fail()");
+ }
+
+ public byte[] awaitEspPacketNoPlaintext(
+ int spi, byte[] plaintext, boolean useEncap, int expectedPacketSize) throws Exception {
+ final byte[] espPkt = awaitPacket(
+ (pkt) -> isEspFailIfSpecifiedPlaintextFound(pkt, spi, useEncap, plaintext));
+
+ // Validate packet size
+ assertEquals(expectedPacketSize, espPkt.length);
+
+ return espPkt; // We've found the packet we're looking for.
+ }
+
+ private static boolean isSpiEqual(byte[] pkt, int espOffset, int spi) {
+ // Check SPI byte by byte.
+ return pkt[espOffset] == (byte) ((spi >>> 24) & 0xff)
+ && pkt[espOffset + 1] == (byte) ((spi >>> 16) & 0xff)
+ && pkt[espOffset + 2] == (byte) ((spi >>> 8) & 0xff)
+ && pkt[espOffset + 3] == (byte) (spi & 0xff);
+ }
+
+ /**
+ * Variant of isEsp that also fails the test if the provided plaintext is found
+ *
+ * @param pkt the packet bytes to verify
+ * @param spi the expected SPI to look for
+ * @param encap whether encap was enabled, and the packet has a UDP header
+ * @param plaintext the plaintext packet before outbound encryption, which MUST not appear in
+ * the provided packet.
+ */
+ private static boolean isEspFailIfSpecifiedPlaintextFound(
+ byte[] pkt, int spi, boolean encap, byte[] plaintext) {
+ if (Collections.indexOfSubList(Arrays.asList(pkt), Arrays.asList(plaintext)) != -1) {
+ fail("Banned plaintext packet found");
+ }
+
+ return isEsp(pkt, spi, encap);
+ }
+
+ private static boolean isEsp(byte[] pkt, int spi, boolean encap) {
+ if (isIpv6(pkt)) {
+ // IPv6 UDP encap not supported by kernels; assume non-encap.
+ return pkt[IP6_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP6_HDRLEN, spi);
+ } else {
+ // Use default IPv4 header length (assuming no options)
+ if (encap) {
+ return pkt[IP4_PROTO_OFFSET] == IPPROTO_UDP
+ && isSpiEqual(pkt, IP4_HDRLEN + UDP_HDRLEN, spi);
+ } else {
+ return pkt[IP4_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP4_HDRLEN, spi);
+ }
+ }
+ }
+
+ public static boolean isIpv6(byte[] pkt) {
+ // First nibble shows IP version. 0x60 for IPv6
+ return (pkt[0] & (byte) 0xF0) == (byte) 0x60;
+ }
+
+ private static byte[] getReflectedPacket(byte[] pkt) {
+ byte[] reflected = Arrays.copyOf(pkt, pkt.length);
+
+ if (isIpv6(pkt)) {
+ // Set reflected packet's dst to that of the original's src
+ System.arraycopy(
+ pkt, // src
+ IP6_ADDR_OFFSET + IP6_ADDR_LEN, // src offset
+ reflected, // dst
+ IP6_ADDR_OFFSET, // dst offset
+ IP6_ADDR_LEN); // len
+ // Set reflected packet's src IP to that of the original's dst IP
+ System.arraycopy(
+ pkt, // src
+ IP6_ADDR_OFFSET, // src offset
+ reflected, // dst
+ IP6_ADDR_OFFSET + IP6_ADDR_LEN, // dst offset
+ IP6_ADDR_LEN); // len
+ } else {
+ // Set reflected packet's dst to that of the original's src
+ System.arraycopy(
+ pkt, // src
+ IP4_ADDR_OFFSET + IP4_ADDR_LEN, // src offset
+ reflected, // dst
+ IP4_ADDR_OFFSET, // dst offset
+ IP4_ADDR_LEN); // len
+ // Set reflected packet's src IP to that of the original's dst IP
+ System.arraycopy(
+ pkt, // src
+ IP4_ADDR_OFFSET, // src offset
+ reflected, // dst
+ IP4_ADDR_OFFSET + IP4_ADDR_LEN, // dst offset
+ IP4_ADDR_LEN); // len
+ }
+ return reflected;
+ }
+
+ /** Takes all captured packets, flips the src/dst, and re-injects them. */
+ public void reflectPackets() throws IOException {
+ synchronized (mPackets) {
+ for (byte[] pkt : mPackets) {
+ injectPacket(getReflectedPacket(pkt));
+ }
+ }
+ }
+
+ public void injectPacket(byte[] pkt) throws IOException {
+ FileOutputStream out = new FileOutputStream(mTunFd.getFileDescriptor());
+ out.write(pkt);
+ out.flush();
+ }
+
+ /** Resets the intercepted packets. */
+ public void reset() throws IOException {
+ synchronized (mPackets) {
+ mPackets.clear();
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/UriTest.java b/tests/cts/net/src/android/net/cts/UriTest.java
new file mode 100644
index 0000000..40b8fb7
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/UriTest.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2008 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.ContentUris;
+import android.net.Uri;
+import android.os.Parcel;
+import android.test.AndroidTestCase;
+import java.io.File;
+import java.util.Arrays;
+import java.util.ArrayList;
+
+public class UriTest extends AndroidTestCase {
+ public void testParcelling() {
+ parcelAndUnparcel(Uri.parse("foo:bob%20lee"));
+ parcelAndUnparcel(Uri.fromParts("foo", "bob lee", "fragment"));
+ parcelAndUnparcel(new Uri.Builder()
+ .scheme("http")
+ .authority("crazybob.org")
+ .path("/rss/")
+ .encodedQuery("a=b")
+ .fragment("foo")
+ .build());
+ }
+
+ private void parcelAndUnparcel(Uri u) {
+ Parcel p = Parcel.obtain();
+ Uri.writeToParcel(p, u);
+ p.setDataPosition(0);
+ assertEquals(u, Uri.CREATOR.createFromParcel(p));
+
+ p.setDataPosition(0);
+ u = u.buildUpon().build();
+ Uri.writeToParcel(p, u);
+ p.setDataPosition(0);
+ assertEquals(u, Uri.CREATOR.createFromParcel(p));
+ }
+
+ public void testBuildUpon() {
+ Uri u = Uri.parse("bob:lee").buildUpon().scheme("robert").build();
+ assertEquals("robert", u.getScheme());
+ assertEquals("lee", u.getEncodedSchemeSpecificPart());
+ assertEquals("lee", u.getSchemeSpecificPart());
+ assertNull(u.getQuery());
+ assertNull(u.getPath());
+ assertNull(u.getAuthority());
+ assertNull(u.getHost());
+
+ Uri a = Uri.fromParts("foo", "bar", "tee");
+ Uri b = a.buildUpon().fragment("new").build();
+ assertEquals("new", b.getFragment());
+ assertEquals("bar", b.getSchemeSpecificPart());
+ assertEquals("foo", b.getScheme());
+ a = new Uri.Builder()
+ .scheme("foo")
+ .encodedOpaquePart("bar")
+ .fragment("tee")
+ .build();
+ b = a.buildUpon().fragment("new").build();
+ assertEquals("new", b.getFragment());
+ assertEquals("bar", b.getSchemeSpecificPart());
+ assertEquals("foo", b.getScheme());
+
+ a = Uri.fromParts("scheme", "[2001:db8::dead:e1f]/foo", "bar");
+ b = a.buildUpon().fragment("qux").build();
+ assertEquals("qux", b.getFragment());
+ assertEquals("[2001:db8::dead:e1f]/foo", b.getSchemeSpecificPart());
+ assertEquals("scheme", b.getScheme());
+ }
+
+ public void testStringUri() {
+ assertEquals("bob lee",
+ Uri.parse("foo:bob%20lee").getSchemeSpecificPart());
+ assertEquals("bob%20lee",
+ Uri.parse("foo:bob%20lee").getEncodedSchemeSpecificPart());
+
+ assertEquals("/bob%20lee",
+ Uri.parse("foo:/bob%20lee").getEncodedPath());
+ assertNull(Uri.parse("foo:bob%20lee").getPath());
+
+ assertEquals("bob%20lee",
+ Uri.parse("foo:?bob%20lee").getEncodedQuery());
+ assertNull(Uri.parse("foo:bob%20lee").getEncodedQuery());
+ assertNull(Uri.parse("foo:bar#?bob%20lee").getQuery());
+
+ assertEquals("bob%20lee",
+ Uri.parse("foo:#bob%20lee").getEncodedFragment());
+
+ Uri uri = Uri.parse("http://localhost:42");
+ assertEquals("localhost", uri.getHost());
+ assertEquals(42, uri.getPort());
+
+ uri = Uri.parse("http://bob@localhost:42");
+ assertEquals("bob", uri.getUserInfo());
+ assertEquals("localhost", uri.getHost());
+ assertEquals(42, uri.getPort());
+
+ uri = Uri.parse("http://bob%20lee@localhost:42");
+ assertEquals("bob lee", uri.getUserInfo());
+ assertEquals("bob%20lee", uri.getEncodedUserInfo());
+
+ uri = Uri.parse("http://localhost");
+ assertEquals("localhost", uri.getHost());
+ assertEquals(-1, uri.getPort());
+
+ uri = Uri.parse("http://a:a@example.com:a@example2.com/path");
+ assertEquals("a:a@example.com:a@example2.com", uri.getAuthority());
+ assertEquals("example2.com", uri.getHost());
+ assertEquals(-1, uri.getPort());
+ assertEquals("/path", uri.getPath());
+
+ uri = Uri.parse("http://a.foo.com\\.example.com/path");
+ assertEquals("a.foo.com", uri.getHost());
+ assertEquals(-1, uri.getPort());
+ assertEquals("\\.example.com/path", uri.getPath());
+
+ uri = Uri.parse("https://[2001:db8::dead:e1f]/foo");
+ assertEquals("[2001:db8::dead:e1f]", uri.getAuthority());
+ assertNull(uri.getUserInfo());
+ assertEquals("[2001:db8::dead:e1f]", uri.getHost());
+ assertEquals(-1, uri.getPort());
+ assertEquals("/foo", uri.getPath());
+ assertEquals(null, uri.getFragment());
+ assertEquals("//[2001:db8::dead:e1f]/foo", uri.getSchemeSpecificPart());
+
+ uri = Uri.parse("https://[2001:db8::dead:e1f]/#foo");
+ assertEquals("[2001:db8::dead:e1f]", uri.getAuthority());
+ assertNull(uri.getUserInfo());
+ assertEquals("[2001:db8::dead:e1f]", uri.getHost());
+ assertEquals(-1, uri.getPort());
+ assertEquals("/", uri.getPath());
+ assertEquals("foo", uri.getFragment());
+ assertEquals("//[2001:db8::dead:e1f]/", uri.getSchemeSpecificPart());
+
+ uri = Uri.parse(
+ "https://some:user@[2001:db8::dead:e1f]:1234/foo?corge=thud&corge=garp#bar");
+ assertEquals("some:user@[2001:db8::dead:e1f]:1234", uri.getAuthority());
+ assertEquals("some:user", uri.getUserInfo());
+ assertEquals("[2001:db8::dead:e1f]", uri.getHost());
+ assertEquals(1234, uri.getPort());
+ assertEquals("/foo", uri.getPath());
+ assertEquals("bar", uri.getFragment());
+ assertEquals("//some:user@[2001:db8::dead:e1f]:1234/foo?corge=thud&corge=garp",
+ uri.getSchemeSpecificPart());
+ assertEquals("corge=thud&corge=garp", uri.getQuery());
+ assertEquals("thud", uri.getQueryParameter("corge"));
+ assertEquals(Arrays.asList("thud", "garp"), uri.getQueryParameters("corge"));
+ }
+
+ public void testCompareTo() {
+ Uri a = Uri.parse("foo:a");
+ Uri b = Uri.parse("foo:b");
+ Uri b2 = Uri.parse("foo:b");
+
+ assertTrue(a.compareTo(b) < 0);
+ assertTrue(b.compareTo(a) > 0);
+ assertEquals(0, b.compareTo(b2));
+ }
+
+ public void testEqualsAndHashCode() {
+ Uri a = Uri.parse("http://crazybob.org/test/?foo=bar#tee");
+
+ Uri b = new Uri.Builder()
+ .scheme("http")
+ .authority("crazybob.org")
+ .path("/test/")
+ .encodedQuery("foo=bar")
+ .fragment("tee")
+ .build();
+
+ // Try alternate builder methods.
+ Uri c = new Uri.Builder()
+ .scheme("http")
+ .encodedAuthority("crazybob.org")
+ .encodedPath("/test/")
+ .encodedQuery("foo=bar")
+ .encodedFragment("tee")
+ .build();
+
+ assertFalse(Uri.EMPTY.equals(null));
+ assertEquals(a, b);
+ assertEquals(b, c);
+ assertEquals(c, a);
+ assertEquals(a.hashCode(), b.hashCode());
+ assertEquals(b.hashCode(), c.hashCode());
+ }
+
+ public void testEncodeAndDecode() {
+ String encoded = Uri.encode("Bob:/", "/");
+ assertEquals(-1, encoded.indexOf(':'));
+ assertTrue(encoded.indexOf('/') > -1);
+ assertEncodeDecodeRoundtripExact(null);
+ assertEncodeDecodeRoundtripExact("");
+ assertEncodeDecodeRoundtripExact("Bob");
+ assertEncodeDecodeRoundtripExact(":Bob");
+ assertEncodeDecodeRoundtripExact("::Bob");
+ assertEncodeDecodeRoundtripExact("Bob::Lee");
+ assertEncodeDecodeRoundtripExact("Bob:Lee");
+ assertEncodeDecodeRoundtripExact("Bob::");
+ assertEncodeDecodeRoundtripExact("Bob:");
+ assertEncodeDecodeRoundtripExact("::Bob::");
+ assertEncodeDecodeRoundtripExact("https:/some:user@[2001:db8::dead:e1f]:1234/foo#bar");
+ }
+
+ private static void assertEncodeDecodeRoundtripExact(String s) {
+ assertEquals(s, Uri.decode(Uri.encode(s, null)));
+ }
+
+ public void testDecode_emptyString_returnsEmptyString() {
+ assertEquals("", Uri.decode(""));
+ }
+
+ public void testDecode_null_returnsNull() {
+ assertNull(Uri.decode(null));
+ }
+
+ public void testDecode_wrongHexDigit() {
+ // %p in the end.
+ assertEquals("ab/$\u0102%\u0840\uFFFD\u0000", Uri.decode("ab%2f$%C4%82%25%e0%a1%80%p"));
+ }
+
+ public void testDecode_secondHexDigitWrong() {
+ // %1p in the end.
+ assertEquals("ab/$\u0102%\u0840\uFFFD\u0001", Uri.decode("ab%2f$%c4%82%25%e0%a1%80%1p"));
+ }
+
+ public void testDecode_endsWithPercent_appendsUnknownCharacter() {
+ // % in the end.
+ assertEquals("ab/$\u0102%\u0840\uFFFD", Uri.decode("ab%2f$%c4%82%25%e0%a1%80%"));
+ }
+
+ public void testDecode_plusNotConverted() {
+ assertEquals("ab/$\u0102%+\u0840", Uri.decode("ab%2f$%c4%82%25+%e0%a1%80"));
+ }
+
+ // Last character needs decoding (make sure we are flushing the buffer with chars to decode).
+ public void testDecode_lastCharacter() {
+ assertEquals("ab/$\u0102%\u0840", Uri.decode("ab%2f$%c4%82%25%e0%a1%80"));
+ }
+
+ // Check that a second row of encoded characters is decoded properly (internal buffers are
+ // reset properly).
+ public void testDecode_secondRowOfEncoded() {
+ assertEquals("ab/$\u0102%\u0840aa\u0840",
+ Uri.decode("ab%2f$%c4%82%25%e0%a1%80aa%e0%a1%80"));
+ }
+
+ public void testFromFile() {
+ File f = new File("/tmp/bob");
+ Uri uri = Uri.fromFile(f);
+ assertEquals("file:///tmp/bob", uri.toString());
+ try {
+ Uri.fromFile(null);
+ fail("testFile fail");
+ } catch (NullPointerException e) {}
+ }
+
+ public void testQueryParameters() {
+ Uri uri = Uri.parse("content://user");
+ assertEquals(null, uri.getQueryParameter("a"));
+
+ uri = uri.buildUpon().appendQueryParameter("a", "b").build();
+ assertEquals("b", uri.getQueryParameter("a"));
+
+ uri = uri.buildUpon().appendQueryParameter("a", "b2").build();
+ assertEquals(Arrays.asList("b", "b2"), uri.getQueryParameters("a"));
+
+ uri = uri.buildUpon().appendQueryParameter("c", "d").build();
+ assertEquals(Arrays.asList("b", "b2"), uri.getQueryParameters("a"));
+ assertEquals("d", uri.getQueryParameter("c"));
+ }
+
+ public void testPathOperations() {
+ Uri uri = Uri.parse("content://user/a/b");
+
+ assertEquals(2, uri.getPathSegments().size());
+ assertEquals("a", uri.getPathSegments().get(0));
+ assertEquals("b", uri.getPathSegments().get(1));
+ assertEquals("b", uri.getLastPathSegment());
+
+ Uri first = uri;
+ uri = uri.buildUpon().appendPath("c").build();
+ assertEquals(3, uri.getPathSegments().size());
+ assertEquals("c", uri.getPathSegments().get(2));
+ assertEquals("c", uri.getLastPathSegment());
+ assertEquals("content://user/a/b/c", uri.toString());
+
+ uri = ContentUris.withAppendedId(uri, 100);
+ assertEquals(4, uri.getPathSegments().size());
+ assertEquals("100", uri.getPathSegments().get(3));
+ assertEquals("100", uri.getLastPathSegment());
+ assertEquals(100, ContentUris.parseId(uri));
+ assertEquals("content://user/a/b/c/100", uri.toString());
+
+ // Make sure the original URI is still intact.
+ assertEquals(2, first.getPathSegments().size());
+ assertEquals("b", first.getLastPathSegment());
+
+ try {
+ first.getPathSegments().get(2);
+ fail("test path operations");
+ } catch (IndexOutOfBoundsException e) {}
+
+ assertEquals(null, Uri.EMPTY.getLastPathSegment());
+
+ Uri withC = Uri.parse("foo:/a/b/").buildUpon().appendPath("c").build();
+ assertEquals("/a/b/c", withC.getPath());
+ }
+
+ public void testOpaqueUri() {
+ Uri uri = Uri.parse("mailto:nobody");
+ testOpaqueUri(uri);
+
+ uri = uri.buildUpon().build();
+ testOpaqueUri(uri);
+
+ uri = Uri.fromParts("mailto", "nobody", null);
+ testOpaqueUri(uri);
+
+ uri = uri.buildUpon().build();
+ testOpaqueUri(uri);
+
+ uri = new Uri.Builder()
+ .scheme("mailto")
+ .opaquePart("nobody")
+ .build();
+ testOpaqueUri(uri);
+
+ uri = uri.buildUpon().build();
+ testOpaqueUri(uri);
+ }
+
+ private void testOpaqueUri(Uri uri) {
+ assertEquals("mailto", uri.getScheme());
+ assertEquals("nobody", uri.getSchemeSpecificPart());
+ assertEquals("nobody", uri.getEncodedSchemeSpecificPart());
+
+ assertNull(uri.getFragment());
+ assertTrue(uri.isAbsolute());
+ assertTrue(uri.isOpaque());
+ assertFalse(uri.isRelative());
+ assertFalse(uri.isHierarchical());
+
+ assertNull(uri.getAuthority());
+ assertNull(uri.getEncodedAuthority());
+ assertNull(uri.getPath());
+ assertNull(uri.getEncodedPath());
+ assertNull(uri.getUserInfo());
+ assertNull(uri.getEncodedUserInfo());
+ assertNull(uri.getQuery());
+ assertNull(uri.getEncodedQuery());
+ assertNull(uri.getHost());
+ assertEquals(-1, uri.getPort());
+
+ assertTrue(uri.getPathSegments().isEmpty());
+ assertNull(uri.getLastPathSegment());
+
+ assertEquals("mailto:nobody", uri.toString());
+
+ Uri withFragment = uri.buildUpon().fragment("top").build();
+ assertEquals("mailto:nobody#top", withFragment.toString());
+ }
+
+ public void testHierarchicalUris() {
+ testHierarchical("http", "google.com", "/p1/p2", "query", "fragment");
+ testHierarchical("file", null, "/p1/p2", null, null);
+ testHierarchical("content", "contact", "/p1/p2", null, null);
+ testHierarchical("http", "google.com", "/p1/p2", null, "fragment");
+ testHierarchical("http", "google.com", "", null, "fragment");
+ testHierarchical("http", "google.com", "", "query", "fragment");
+ testHierarchical("http", "google.com", "", "query", null);
+ testHierarchical("http", null, "/", "query", null);
+ }
+
+ private static void testHierarchical(String scheme, String authority,
+ String path, String query, String fragment) {
+ StringBuilder sb = new StringBuilder();
+
+ if (authority != null) {
+ sb.append("//").append(authority);
+ }
+ if (path != null) {
+ sb.append(path);
+ }
+ if (query != null) {
+ sb.append('?').append(query);
+ }
+
+ String ssp = sb.toString();
+
+ if (scheme != null) {
+ sb.insert(0, scheme + ":");
+ }
+ if (fragment != null) {
+ sb.append('#').append(fragment);
+ }
+
+ String uriString = sb.toString();
+
+ Uri uri = Uri.parse(uriString);
+
+ // Run these twice to test caching.
+ compareHierarchical(
+ uriString, ssp, uri, scheme, authority, path, query, fragment);
+ compareHierarchical(
+ uriString, ssp, uri, scheme, authority, path, query, fragment);
+
+ // Test rebuilt version.
+ uri = uri.buildUpon().build();
+
+ // Run these twice to test caching.
+ compareHierarchical(
+ uriString, ssp, uri, scheme, authority, path, query, fragment);
+ compareHierarchical(
+ uriString, ssp, uri, scheme, authority, path, query, fragment);
+
+ // The decoded and encoded versions of the inputs are all the same.
+ // We'll test the actual encoding decoding separately.
+
+ // Test building with encoded versions.
+ Uri built = new Uri.Builder()
+ .scheme(scheme)
+ .encodedAuthority(authority)
+ .encodedPath(path)
+ .encodedQuery(query)
+ .encodedFragment(fragment)
+ .build();
+
+ compareHierarchical(
+ uriString, ssp, built, scheme, authority, path, query, fragment);
+ compareHierarchical(
+ uriString, ssp, built, scheme, authority, path, query, fragment);
+
+ // Test building with decoded versions.
+ built = new Uri.Builder()
+ .scheme(scheme)
+ .authority(authority)
+ .path(path)
+ .query(query)
+ .fragment(fragment)
+ .build();
+
+ compareHierarchical(
+ uriString, ssp, built, scheme, authority, path, query, fragment);
+ compareHierarchical(
+ uriString, ssp, built, scheme, authority, path, query, fragment);
+
+ // Rebuild.
+ built = built.buildUpon().build();
+
+ compareHierarchical(
+ uriString, ssp, built, scheme, authority, path, query, fragment);
+ compareHierarchical(
+ uriString, ssp, built, scheme, authority, path, query, fragment);
+ }
+
+ private static void compareHierarchical(String uriString, String ssp,
+ Uri uri,
+ String scheme, String authority, String path, String query,
+ String fragment) {
+ assertEquals(scheme, uri.getScheme());
+ assertEquals(authority, uri.getAuthority());
+ assertEquals(authority, uri.getEncodedAuthority());
+ assertEquals(path, uri.getPath());
+ assertEquals(path, uri.getEncodedPath());
+ assertEquals(query, uri.getQuery());
+ assertEquals(query, uri.getEncodedQuery());
+ assertEquals(fragment, uri.getFragment());
+ assertEquals(fragment, uri.getEncodedFragment());
+ assertEquals(ssp, uri.getSchemeSpecificPart());
+
+ if (scheme != null) {
+ assertTrue(uri.isAbsolute());
+ assertFalse(uri.isRelative());
+ } else {
+ assertFalse(uri.isAbsolute());
+ assertTrue(uri.isRelative());
+ }
+
+ assertFalse(uri.isOpaque());
+ assertTrue(uri.isHierarchical());
+ assertEquals(uriString, uri.toString());
+ }
+
+ public void testNormalizeScheme() {
+ assertEquals(Uri.parse(""), Uri.parse("").normalizeScheme());
+ assertEquals(Uri.parse("http://www.android.com"),
+ Uri.parse("http://www.android.com").normalizeScheme());
+ assertEquals(Uri.parse("http://USER@WWW.ANDROID.COM:100/ABOUT?foo=blah@bar=bleh#c"),
+ Uri.parse("HTTP://USER@WWW.ANDROID.COM:100/ABOUT?foo=blah@bar=bleh#c")
+ .normalizeScheme());
+ }
+
+ public void testToSafeString_tel() {
+ checkToSafeString("tel:xxxxxx", "tel:Google");
+ checkToSafeString("tel:xxxxxxxxxx", "tel:1234567890");
+ checkToSafeString("tEl:xxx.xxx-xxxx", "tEl:123.456-7890");
+ }
+
+ public void testToSafeString_sip() {
+ checkToSafeString("sip:xxxxxxx@xxxxxxx.xxxxxxxx", "sip:android@android.com:1234");
+ checkToSafeString("sIp:xxxxxxx@xxxxxxx.xxx", "sIp:android@android.com");
+ }
+
+ public void testToSafeString_sms() {
+ checkToSafeString("sms:xxxxxx", "sms:123abc");
+ checkToSafeString("smS:xxx.xxx-xxxx", "smS:123.456-7890");
+ }
+
+ public void testToSafeString_smsto() {
+ checkToSafeString("smsto:xxxxxx", "smsto:123abc");
+ checkToSafeString("SMSTo:xxx.xxx-xxxx", "SMSTo:123.456-7890");
+ }
+
+ public void testToSafeString_mailto() {
+ checkToSafeString("mailto:xxxxxxx@xxxxxxx.xxx", "mailto:android@android.com");
+ checkToSafeString("Mailto:xxxxxxx@xxxxxxx.xxxxxxxxxx",
+ "Mailto:android@android.com/secret");
+ }
+
+ public void testToSafeString_nfc() {
+ checkToSafeString("nfc:xxxxxx", "nfc:123abc");
+ checkToSafeString("nfc:xxx.xxx-xxxx", "nfc:123.456-7890");
+ checkToSafeString("nfc:xxxxxxx@xxxxxxx.xxx", "nfc:android@android.com");
+ }
+
+ public void testToSafeString_http() {
+ checkToSafeString("http://www.android.com/...", "http://www.android.com");
+ checkToSafeString("HTTP://www.android.com/...", "HTTP://www.android.com");
+ checkToSafeString("http://www.android.com/...", "http://www.android.com/");
+ checkToSafeString("http://www.android.com/...", "http://www.android.com/secretUrl?param");
+ checkToSafeString("http://www.android.com/...",
+ "http://user:pwd@www.android.com/secretUrl?param");
+ checkToSafeString("http://www.android.com/...",
+ "http://user@www.android.com/secretUrl?param");
+ checkToSafeString("http://www.android.com/...", "http://www.android.com/secretUrl?param");
+ checkToSafeString("http:///...", "http:///path?param");
+ checkToSafeString("http:///...", "http://");
+ checkToSafeString("http://:12345/...", "http://:12345/");
+ }
+
+ public void testToSafeString_https() {
+ checkToSafeString("https://www.android.com/...", "https://www.android.com/secretUrl?param");
+ checkToSafeString("https://www.android.com:8443/...",
+ "https://user:pwd@www.android.com:8443/secretUrl?param");
+ checkToSafeString("https://www.android.com/...", "https://user:pwd@www.android.com");
+ checkToSafeString("Https://www.android.com/...", "Https://user:pwd@www.android.com");
+ }
+
+ public void testToSafeString_ftp() {
+ checkToSafeString("ftp://ftp.android.com/...", "ftp://ftp.android.com/");
+ checkToSafeString("ftP://ftp.android.com/...", "ftP://anonymous@ftp.android.com/");
+ checkToSafeString("ftp://ftp.android.com:2121/...",
+ "ftp://root:love@ftp.android.com:2121/");
+ }
+
+ public void testToSafeString_rtsp() {
+ checkToSafeString("rtsp://rtsp.android.com/...", "rtsp://rtsp.android.com/");
+ checkToSafeString("rtsp://rtsp.android.com/...", "rtsp://rtsp.android.com/video.mov");
+ checkToSafeString("rtsp://rtsp.android.com/...", "rtsp://rtsp.android.com/video.mov?param");
+ checkToSafeString("RtsP://rtsp.android.com/...", "RtsP://anonymous@rtsp.android.com/");
+ checkToSafeString("rtsp://rtsp.android.com:2121/...",
+ "rtsp://username:password@rtsp.android.com:2121/");
+ }
+
+ public void testToSafeString_notSupport() {
+ checkToSafeString("unsupported://ajkakjah/askdha/secret?secret",
+ "unsupported://ajkakjah/askdha/secret?secret");
+ checkToSafeString("unsupported:ajkakjah/askdha/secret?secret",
+ "unsupported:ajkakjah/askdha/secret?secret");
+ }
+
+ private void checkToSafeString(String expectedSafeString, String original) {
+ assertEquals(expectedSafeString, Uri.parse(original).toSafeString());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/Uri_BuilderTest.java b/tests/cts/net/src/android/net/cts/Uri_BuilderTest.java
new file mode 100644
index 0000000..4088d82
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/Uri_BuilderTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2008 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 junit.framework.TestCase;
+import android.net.Uri.Builder;
+import android.net.Uri;
+
+public class Uri_BuilderTest extends TestCase {
+ public void testBuilderOperations() {
+ Uri uri = Uri.parse("http://google.com/p1?query#fragment");
+ Builder builder = uri.buildUpon();
+ uri = builder.appendPath("p2").build();
+ assertEquals("http", uri.getScheme());
+ assertEquals("google.com", uri.getAuthority());
+ assertEquals("/p1/p2", uri.getPath());
+ assertEquals("query", uri.getQuery());
+ assertEquals("fragment", uri.getFragment());
+ assertEquals(uri.toString(), builder.toString());
+
+ uri = Uri.parse("mailto:nobody");
+ builder = uri.buildUpon();
+ uri = builder.build();
+ assertEquals("mailto", uri.getScheme());
+ assertEquals("nobody", uri.getSchemeSpecificPart());
+ assertEquals(uri.toString(), builder.toString());
+
+ uri = new Uri.Builder()
+ .scheme("http")
+ .encodedAuthority("google.com")
+ .encodedPath("/p1")
+ .appendEncodedPath("p2")
+ .encodedQuery("query")
+ .appendQueryParameter("query2", null)
+ .encodedFragment("fragment")
+ .build();
+ assertEquals("http", uri.getScheme());
+ assertEquals("google.com", uri.getEncodedAuthority());
+ assertEquals("/p1/p2", uri.getEncodedPath());
+ assertEquals("query&query2=null", uri.getEncodedQuery());
+ assertEquals("fragment", uri.getEncodedFragment());
+
+ uri = new Uri.Builder()
+ .scheme("mailto")
+ .encodedOpaquePart("nobody")
+ .build();
+ assertEquals("mailto", uri.getScheme());
+ assertEquals("nobody", uri.getEncodedSchemeSpecificPart());
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/UrlQuerySanitizerTest.java b/tests/cts/net/src/android/net/cts/UrlQuerySanitizerTest.java
new file mode 100644
index 0000000..5a70928
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/UrlQuerySanitizerTest.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2009 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.UrlQuerySanitizer;
+import android.net.UrlQuerySanitizer.IllegalCharacterValueSanitizer;
+import android.net.UrlQuerySanitizer.ParameterValuePair;
+import android.net.UrlQuerySanitizer.ValueSanitizer;
+import android.os.Build;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Set;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class UrlQuerySanitizerTest {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+ private static final int ALL_OK = IllegalCharacterValueSanitizer.ALL_OK;
+
+ // URL for test.
+ private static final String TEST_URL = "http://example.com/?name=Joe+User&age=20&height=175";
+
+ // Default sanitizer's change when "+".
+ private static final String EXPECTED_UNDERLINE_NAME = "Joe_User";
+
+ // IllegalCharacterValueSanitizer sanitizer's change when "+".
+ private static final String EXPECTED_SPACE_NAME = "Joe User";
+ private static final String EXPECTED_AGE = "20";
+ private static final String EXPECTED_HEIGHT = "175";
+ private static final String NAME = "name";
+ private static final String AGE = "age";
+ private static final String HEIGHT = "height";
+
+ @Test
+ public void testUrlQuerySanitizer() {
+ MockUrlQuerySanitizer uqs = new MockUrlQuerySanitizer();
+ assertFalse(uqs.getAllowUnregisteredParamaters());
+
+ final String query = "book=thinking in java&price=108";
+ final String book = "book";
+ final String bookName = "thinking in java";
+ final String price = "price";
+ final String bookPrice = "108";
+ final String notExistPar = "notExistParameter";
+ uqs.registerParameters(new String[]{book, price}, UrlQuerySanitizer.getSpaceLegal());
+ uqs.parseQuery(query);
+ assertTrue(uqs.hasParameter(book));
+ assertTrue(uqs.hasParameter(price));
+ assertFalse(uqs.hasParameter(notExistPar));
+ assertEquals(bookName, uqs.getValue(book));
+ assertEquals(bookPrice, uqs.getValue(price));
+ assertNull(uqs.getValue(notExistPar));
+ uqs.clear();
+ assertFalse(uqs.hasParameter(book));
+ assertFalse(uqs.hasParameter(price));
+
+ uqs.parseEntry(book, bookName);
+ assertTrue(uqs.hasParameter(book));
+ assertEquals(bookName, uqs.getValue(book));
+ uqs.parseEntry(price, bookPrice);
+ assertTrue(uqs.hasParameter(price));
+ assertEquals(bookPrice, uqs.getValue(price));
+ assertFalse(uqs.hasParameter(notExistPar));
+ assertNull(uqs.getValue(notExistPar));
+
+ uqs = new MockUrlQuerySanitizer(TEST_URL);
+ assertTrue(uqs.getAllowUnregisteredParamaters());
+
+ assertTrue(uqs.hasParameter(NAME));
+ assertTrue(uqs.hasParameter(AGE));
+ assertTrue(uqs.hasParameter(HEIGHT));
+ assertFalse(uqs.hasParameter(notExistPar));
+
+ assertEquals(EXPECTED_UNDERLINE_NAME, uqs.getValue(NAME));
+ assertEquals(EXPECTED_AGE, uqs.getValue(AGE));
+ assertEquals(EXPECTED_HEIGHT, uqs.getValue(HEIGHT));
+ assertNull(uqs.getValue(notExistPar));
+
+ final int ContainerLen = 3;
+ Set<String> urlSet = uqs.getParameterSet();
+ assertEquals(ContainerLen, urlSet.size());
+ assertTrue(urlSet.contains(NAME));
+ assertTrue(urlSet.contains(AGE));
+ assertTrue(urlSet.contains(HEIGHT));
+ assertFalse(urlSet.contains(notExistPar));
+
+ List<ParameterValuePair> urlList = uqs.getParameterList();
+ assertEquals(ContainerLen, urlList.size());
+ ParameterValuePair pvp = urlList.get(0);
+ assertEquals(NAME, pvp.mParameter);
+ assertEquals(EXPECTED_UNDERLINE_NAME, pvp.mValue);
+ pvp = urlList.get(1);
+ assertEquals(AGE, pvp.mParameter);
+ assertEquals(EXPECTED_AGE, pvp.mValue);
+ pvp = urlList.get(2);
+ assertEquals(HEIGHT, pvp.mParameter);
+ assertEquals(EXPECTED_HEIGHT, pvp.mValue);
+
+ assertFalse(uqs.getPreferFirstRepeatedParameter());
+ uqs.addSanitizedEntry(HEIGHT, EXPECTED_HEIGHT + 1);
+ assertEquals(ContainerLen, urlSet.size());
+ assertEquals(ContainerLen + 1, urlList.size());
+ assertEquals(EXPECTED_HEIGHT + 1, uqs.getValue(HEIGHT));
+
+ uqs.setPreferFirstRepeatedParameter(true);
+ assertTrue(uqs.getPreferFirstRepeatedParameter());
+ uqs.addSanitizedEntry(HEIGHT, EXPECTED_HEIGHT);
+ assertEquals(ContainerLen, urlSet.size());
+ assertEquals(ContainerLen + 2, urlList.size());
+ assertEquals(EXPECTED_HEIGHT + 1, uqs.getValue(HEIGHT));
+
+ uqs.registerParameter(NAME, null);
+ assertNull(uqs.getValueSanitizer(NAME));
+ assertNotNull(uqs.getEffectiveValueSanitizer(NAME));
+
+ uqs.setAllowUnregisteredParamaters(false);
+ assertFalse(uqs.getAllowUnregisteredParamaters());
+ uqs.registerParameter(NAME, null);
+ assertNull(uqs.getEffectiveValueSanitizer(NAME));
+
+ ValueSanitizer vs = new IllegalCharacterValueSanitizer(ALL_OK);
+ uqs.registerParameter(NAME, vs);
+ uqs.parseUrl(TEST_URL);
+ assertEquals(EXPECTED_SPACE_NAME, uqs.getValue(NAME));
+ assertNotSame(EXPECTED_AGE, uqs.getValue(AGE));
+
+ String[] register = {NAME, AGE};
+ uqs.registerParameters(register, vs);
+ uqs.parseUrl(TEST_URL);
+ assertEquals(EXPECTED_SPACE_NAME, uqs.getValue(NAME));
+ assertEquals(EXPECTED_AGE, uqs.getValue(AGE));
+ assertNotSame(EXPECTED_HEIGHT, uqs.getValue(HEIGHT));
+
+ uqs.setUnregisteredParameterValueSanitizer(vs);
+ assertEquals(vs, uqs.getUnregisteredParameterValueSanitizer());
+
+ vs = UrlQuerySanitizer.getAllIllegal();
+ assertEquals("Joe_User", vs.sanitize("Joe<User"));
+ vs = UrlQuerySanitizer.getAllButNulAndAngleBracketsLegal();
+ assertEquals("Joe User", vs.sanitize("Joe<>\0User"));
+ vs = UrlQuerySanitizer.getAllButNulLegal();
+ assertEquals("Joe User", vs.sanitize("Joe\0User"));
+ vs = UrlQuerySanitizer.getAllButWhitespaceLegal();
+ assertEquals("Joe_User", vs.sanitize("Joe User"));
+ vs = UrlQuerySanitizer.getAmpAndSpaceLegal();
+ assertEquals("Joe User&", vs.sanitize("Joe User&"));
+ vs = UrlQuerySanitizer.getAmpLegal();
+ assertEquals("Joe_User&", vs.sanitize("Joe User&"));
+ vs = UrlQuerySanitizer.getSpaceLegal();
+ assertEquals("Joe User ", vs.sanitize("Joe User&"));
+ vs = UrlQuerySanitizer.getUrlAndSpaceLegal();
+ assertEquals("Joe User&Smith%B5'\'", vs.sanitize("Joe User&Smith%B5'\'"));
+ vs = UrlQuerySanitizer.getUrlLegal();
+ assertEquals("Joe_User&Smith%B5'\'", vs.sanitize("Joe User&Smith%B5'\'"));
+
+ String escape = "Joe";
+ assertEquals(escape, uqs.unescape(escape));
+ String expectedPlus = "Joe User";
+ String expectedPercentSignHex = "title=" + Character.toString((char)181);
+ String initialPlus = "Joe+User";
+ String initialPercentSign = "title=%B5";
+ assertEquals(expectedPlus, uqs.unescape(initialPlus));
+ assertEquals(expectedPercentSignHex, uqs.unescape(initialPercentSign));
+ String expectedPlusThenPercentSign = "Joe Random, User";
+ String plusThenPercentSign = "Joe+Random%2C%20User";
+ assertEquals(expectedPlusThenPercentSign, uqs.unescape(plusThenPercentSign));
+ String expectedPercentSignThenPlus = "Joe, Random User";
+ String percentSignThenPlus = "Joe%2C+Random+User";
+ assertEquals(expectedPercentSignThenPlus, uqs.unescape(percentSignThenPlus));
+
+ assertTrue(uqs.decodeHexDigit('0') >= 0);
+ assertTrue(uqs.decodeHexDigit('b') >= 0);
+ assertTrue(uqs.decodeHexDigit('F') >= 0);
+ assertTrue(uqs.decodeHexDigit('$') < 0);
+
+ assertTrue(uqs.isHexDigit('0'));
+ assertTrue(uqs.isHexDigit('b'));
+ assertTrue(uqs.isHexDigit('F'));
+ assertFalse(uqs.isHexDigit('$'));
+
+ uqs.clear();
+ assertEquals(0, urlSet.size());
+ assertEquals(0, urlList.size());
+
+ uqs.setPreferFirstRepeatedParameter(true);
+ assertTrue(uqs.getPreferFirstRepeatedParameter());
+ uqs.setPreferFirstRepeatedParameter(false);
+ assertFalse(uqs.getPreferFirstRepeatedParameter());
+
+ UrlQuerySanitizer uq = new UrlQuerySanitizer();
+ uq.setPreferFirstRepeatedParameter(true);
+ final String PARA_ANSWER = "answer";
+ uq.registerParameter(PARA_ANSWER, new MockValueSanitizer());
+ uq.parseUrl("http://www.google.com/question?answer=13&answer=42");
+ assertEquals("13", uq.getValue(PARA_ANSWER));
+
+ uq.setPreferFirstRepeatedParameter(false);
+ uq.parseQuery("http://www.google.com/question?answer=13&answer=42");
+ assertEquals("42", uq.getValue(PARA_ANSWER));
+
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q) // Only fixed in R
+ public void testScriptUrlOk_73822755() {
+ ValueSanitizer sanitizer = new UrlQuerySanitizer.IllegalCharacterValueSanitizer(
+ UrlQuerySanitizer.IllegalCharacterValueSanitizer.SCRIPT_URL_OK);
+ assertEquals("javascript:alert()", sanitizer.sanitize("javascript:alert()"));
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q) // Only fixed in R
+ public void testScriptUrlBlocked_73822755() {
+ ValueSanitizer sanitizer = UrlQuerySanitizer.getUrlAndSpaceLegal();
+ assertEquals("", sanitizer.sanitize("javascript:alert()"));
+ }
+
+ private static class MockValueSanitizer implements ValueSanitizer{
+
+ public String sanitize(String value) {
+ return value;
+ }
+ }
+
+ class MockUrlQuerySanitizer extends UrlQuerySanitizer {
+ public MockUrlQuerySanitizer() {
+ super();
+ }
+
+ public MockUrlQuerySanitizer(String url) {
+ super(url);
+ }
+
+ @Override
+ protected void addSanitizedEntry(String parameter, String value) {
+ super.addSanitizedEntry(parameter, value);
+ }
+
+ @Override
+ protected void clear() {
+ super.clear();
+ }
+
+ @Override
+ protected int decodeHexDigit(char c) {
+ return super.decodeHexDigit(c);
+ }
+
+ @Override
+ protected boolean isHexDigit(char c) {
+ return super.isHexDigit(c);
+ }
+
+ @Override
+ protected void parseEntry(String parameter, String value) {
+ super.parseEntry(parameter, value);
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_IllegalCharacterValueSanitizerTest.java b/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_IllegalCharacterValueSanitizerTest.java
new file mode 100644
index 0000000..f86af31
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_IllegalCharacterValueSanitizerTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2009 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.UrlQuerySanitizer;
+import android.net.UrlQuerySanitizer.IllegalCharacterValueSanitizer;
+import android.test.AndroidTestCase;
+
+public class UrlQuerySanitizer_IllegalCharacterValueSanitizerTest extends AndroidTestCase {
+ static final int SPACE_OK = IllegalCharacterValueSanitizer.SPACE_OK;
+ public void testSanitize() {
+ IllegalCharacterValueSanitizer sanitizer = new IllegalCharacterValueSanitizer(SPACE_OK);
+ assertEquals("Joe User", sanitizer.sanitize("Joe<User"));
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_ParameterValuePairTest.java b/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_ParameterValuePairTest.java
new file mode 100644
index 0000000..077cdaf
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_ParameterValuePairTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2009 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.UrlQuerySanitizer;
+import android.net.UrlQuerySanitizer.ParameterValuePair;
+import android.test.AndroidTestCase;
+
+public class UrlQuerySanitizer_ParameterValuePairTest extends AndroidTestCase {
+ public void testConstructor() {
+ final String parameter = "name";
+ final String vaule = "Joe_user";
+
+ UrlQuerySanitizer uqs = new UrlQuerySanitizer();
+ ParameterValuePair parameterValuePair = uqs.new ParameterValuePair(parameter, vaule);
+ assertEquals(parameter, parameterValuePair.mParameter);
+ assertEquals(vaule, parameterValuePair.mValue);
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/VpnServiceTest.java b/tests/cts/net/src/android/net/cts/VpnServiceTest.java
new file mode 100644
index 0000000..15af23c
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/VpnServiceTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.Intent;
+import android.net.VpnService;
+import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.AppModeFull;
+import android.test.AndroidTestCase;
+
+import java.io.File;
+import java.net.DatagramSocket;
+import java.net.Socket;
+
+/**
+ * VpnService API is built with security in mind. However, its security also
+ * blocks us from writing tests for positive cases. For now we only test for
+ * negative cases, and we will try to cover the rest in the future.
+ */
+public class VpnServiceTest extends AndroidTestCase {
+
+ private static final String TAG = VpnServiceTest.class.getSimpleName();
+
+ private VpnService mVpnService = new VpnService();
+
+ @AppModeFull(reason = "PackageManager#queryIntentActivities cannot access in instant app mode")
+ public void testPrepare() throws Exception {
+ // Should never return null since we are not prepared.
+ Intent intent = VpnService.prepare(mContext);
+ assertNotNull(intent);
+
+ // Should be always resolved by only one activity.
+ int count = mContext.getPackageManager().queryIntentActivities(intent, 0).size();
+ assertEquals(1, count);
+ }
+
+ public void testEstablish() throws Exception {
+ ParcelFileDescriptor descriptor = null;
+ try {
+ // Should always return null since we are not prepared.
+ descriptor = mVpnService.new Builder().addAddress("8.8.8.8", 30).establish();
+ assertNull(descriptor);
+ } finally {
+ try {
+ descriptor.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testProtect_DatagramSocket() throws Exception {
+ DatagramSocket socket = new DatagramSocket();
+ try {
+ // Should always return false since we are not prepared.
+ assertFalse(mVpnService.protect(socket));
+ } finally {
+ try {
+ socket.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+
+ public void testProtect_Socket() throws Exception {
+ Socket socket = new Socket();
+ try {
+ // Should always return false since we are not prepared.
+ assertFalse(mVpnService.protect(socket));
+ } finally {
+ try {
+ socket.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testProtect_int() throws Exception {
+ DatagramSocket socket = new DatagramSocket();
+ ParcelFileDescriptor descriptor = ParcelFileDescriptor.fromDatagramSocket(socket);
+ try {
+ // Should always return false since we are not prepared.
+ assertFalse(mVpnService.protect(descriptor.getFd()));
+ } finally {
+ try {
+ descriptor.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ try {
+ socket.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+
+ public void testTunDevice() throws Exception {
+ File file = new File("/dev/tun");
+ assertTrue(file.exists());
+ assertFalse(file.isFile());
+ assertFalse(file.isDirectory());
+ assertFalse(file.canExecute());
+ assertFalse(file.canRead());
+ assertFalse(file.canWrite());
+ }
+}
diff --git a/tests/cts/net/src/android/net/ipv6/cts/PingTest.java b/tests/cts/net/src/android/net/ipv6/cts/PingTest.java
new file mode 100644
index 0000000..146fd83
--- /dev/null
+++ b/tests/cts/net/src/android/net/ipv6/cts/PingTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2013 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.ipv6.cts;
+
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructTimeval;
+import static android.system.OsConstants.*;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * Checks that the device has kernel support for the IPv6 ping socket. This allows ping6 to work
+ * without root privileges. The necessary kernel code is in Linux 3.11 or above, or the
+ * <code>common/android-3.x</code> kernel trees. If you are not running one of these kernels, the
+ * functionality can be obtained by cherry-picking the following patches from David Miller's
+ * <code>net-next</code> tree:
+ * <ul>
+ * <li>6d0bfe2 net: ipv6: Add IPv6 support to the ping socket.
+ * <li>c26d6b4 ping: always initialize ->sin6_scope_id and ->sin6_flowinfo
+ * <li>fbfe80c net: ipv6: fix wrong ping_v6_sendmsg return value
+ * <li>a1bdc45 net: ipv6: add missing lock in ping_v6_sendmsg
+ * <li>cf970c0 ping: prevent NULL pointer dereference on write to msg_name
+ * </ul>
+ * or the equivalent backports to the <code>common/android-3.x</code> trees.
+ */
+public class PingTest extends AndroidTestCase {
+ /** Maximum size of the packets we're using to test. */
+ private static final int MAX_SIZE = 4096;
+
+ /** Size of the ICMPv6 header. */
+ private static final int ICMP_HEADER_SIZE = 8;
+
+ /** Number of packets to test. */
+ private static final int NUM_PACKETS = 10;
+
+ /** The beginning of an ICMPv6 echo request: type, code, and uninitialized checksum. */
+ private static final byte[] PING_HEADER = new byte[] {
+ (byte) ICMP6_ECHO_REQUEST, (byte) 0x00, (byte) 0x00, (byte) 0x00
+ };
+
+ /**
+ * Returns a byte array containing an ICMPv6 echo request with the specified payload length.
+ */
+ private byte[] pingPacket(int payloadLength) {
+ byte[] packet = new byte[payloadLength + ICMP_HEADER_SIZE];
+ new Random().nextBytes(packet);
+ System.arraycopy(PING_HEADER, 0, packet, 0, PING_HEADER.length);
+ return packet;
+ }
+
+ /**
+ * Checks that the first length bytes of two byte arrays are equal.
+ */
+ private void assertArrayBytesEqual(byte[] expected, byte[] actual, int length) {
+ for (int i = 0; i < length; i++) {
+ assertEquals("Arrays differ at index " + i + ":", expected[i], actual[i]);
+ }
+ }
+
+ /**
+ * Creates an IPv6 ping socket and sets a receive timeout of 100ms.
+ */
+ private FileDescriptor createPingSocket() throws ErrnoException {
+ FileDescriptor s = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
+ Os.setsockoptTimeval(s, SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(100));
+ return s;
+ }
+
+ /**
+ * Sends a ping packet to a random port on the specified address on the specified socket.
+ */
+ private void sendPing(FileDescriptor s,
+ InetAddress address, byte[] packet) throws ErrnoException, IOException {
+ // Pick a random port. Choose a range that gives a reasonable chance of picking a low port.
+ int port = (int) (Math.random() * 2048);
+
+ // Send the packet.
+ int ret = Os.sendto(s, ByteBuffer.wrap(packet), 0, address, port);
+ assertEquals(packet.length, ret);
+ }
+
+ /**
+ * Checks that a socket has received a response appropriate to the specified packet.
+ */
+ private void checkResponse(FileDescriptor s, InetAddress dest,
+ byte[] sent, boolean useRecvfrom) throws ErrnoException, IOException {
+ ByteBuffer responseBuffer = ByteBuffer.allocate(MAX_SIZE);
+ int bytesRead;
+
+ // Receive the response.
+ if (useRecvfrom) {
+ InetSocketAddress from = new InetSocketAddress();
+ bytesRead = Os.recvfrom(s, responseBuffer, 0, from);
+
+ // Check the source address and scope ID.
+ assertTrue(from.getAddress() instanceof Inet6Address);
+ Inet6Address fromAddress = (Inet6Address) from.getAddress();
+ assertEquals(0, fromAddress.getScopeId());
+ assertNull(fromAddress.getScopedInterface());
+ assertEquals(dest.getHostAddress(), fromAddress.getHostAddress());
+ } else {
+ bytesRead = Os.read(s, responseBuffer);
+ }
+
+ // Check the packet length.
+ assertEquals(sent.length, bytesRead);
+
+ // Check the response is an echo reply.
+ byte[] response = new byte[bytesRead];
+ responseBuffer.flip();
+ responseBuffer.get(response, 0, bytesRead);
+ assertEquals((byte) ICMP6_ECHO_REPLY, response[0]);
+
+ // Find out what ICMP ID was used in the packet that was sent.
+ int id = ((InetSocketAddress) Os.getsockname(s)).getPort();
+ sent[4] = (byte) (id / 256);
+ sent[5] = (byte) (id % 256);
+
+ // Ensure the response is the same as the packet, except for the type (which is 0x81)
+ // and the ID and checksum, which are set by the kernel.
+ response[0] = (byte) 0x80; // Type.
+ response[2] = response[3] = (byte) 0x00; // Checksum.
+ assertArrayBytesEqual(response, sent, bytesRead);
+ }
+
+ /**
+ * Sends NUM_PACKETS random ping packets to ::1 and checks the replies.
+ */
+ public void testLoopbackPing() throws ErrnoException, IOException {
+ // Generate a random ping packet and send it to localhost.
+ InetAddress ipv6Loopback = InetAddress.getByName(null);
+ assertEquals("::1", ipv6Loopback.getHostAddress());
+
+ for (int i = 0; i < NUM_PACKETS; i++) {
+ byte[] packet = pingPacket((int) (Math.random() * (MAX_SIZE - ICMP_HEADER_SIZE)));
+ FileDescriptor s = createPingSocket();
+ // Use both recvfrom and read().
+ sendPing(s, ipv6Loopback, packet);
+ checkResponse(s, ipv6Loopback, packet, true);
+ sendPing(s, ipv6Loopback, packet);
+ checkResponse(s, ipv6Loopback, packet, false);
+ // Check closing the socket doesn't raise an exception.
+ Os.close(s);
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/rtp/cts/AudioCodecTest.java b/tests/cts/net/src/android/net/rtp/cts/AudioCodecTest.java
new file mode 100644
index 0000000..412498c
--- /dev/null
+++ b/tests/cts/net/src/android/net/rtp/cts/AudioCodecTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.rtp.cts;
+
+import android.net.rtp.AudioCodec;
+import android.test.AndroidTestCase;
+
+public class AudioCodecTest extends AndroidTestCase {
+
+ private void assertEquals(AudioCodec codec, int type, String rtpmap, String fmtp) {
+ if (type >= 0) {
+ assertEquals(codec.type, type);
+ } else {
+ assertTrue(codec.type >= 96 && codec.type <= 127);
+ }
+ assertEquals(codec.rtpmap.compareToIgnoreCase(rtpmap), 0);
+ assertEquals(codec.fmtp, fmtp);
+ }
+
+ public void testConstants() throws Exception {
+ assertEquals(AudioCodec.PCMU, 0, "PCMU/8000", null);
+ assertEquals(AudioCodec.PCMA, 8, "PCMA/8000", null);
+ assertEquals(AudioCodec.GSM, 3, "GSM/8000", null);
+ assertEquals(AudioCodec.GSM_EFR, -1, "GSM-EFR/8000", null);
+ assertEquals(AudioCodec.AMR, -1, "AMR/8000", null);
+
+ assertFalse(AudioCodec.AMR.type == AudioCodec.GSM_EFR.type);
+ }
+
+ public void testGetCodec() throws Exception {
+ // Bad types.
+ assertNull(AudioCodec.getCodec(128, "PCMU/8000", null));
+ assertNull(AudioCodec.getCodec(-1, "PCMU/8000", null));
+ assertNull(AudioCodec.getCodec(96, null, null));
+
+ // Fixed types.
+ assertEquals(AudioCodec.getCodec(0, null, null), 0, "PCMU/8000", null);
+ assertEquals(AudioCodec.getCodec(8, null, null), 8, "PCMA/8000", null);
+ assertEquals(AudioCodec.getCodec(3, null, null), 3, "GSM/8000", null);
+
+ // Dynamic types.
+ assertEquals(AudioCodec.getCodec(96, "pcmu/8000", null), 96, "PCMU/8000", null);
+ assertEquals(AudioCodec.getCodec(97, "pcma/8000", null), 97, "PCMA/8000", null);
+ assertEquals(AudioCodec.getCodec(98, "gsm/8000", null), 98, "GSM/8000", null);
+ assertEquals(AudioCodec.getCodec(99, "gsm-efr/8000", null), 99, "GSM-EFR/8000", null);
+ assertEquals(AudioCodec.getCodec(100, "amr/8000", null), 100, "AMR/8000", null);
+ }
+
+ public void testGetCodecs() throws Exception {
+ AudioCodec[] codecs = AudioCodec.getCodecs();
+ assertTrue(codecs.length >= 5);
+
+ // The types of the codecs should be different.
+ boolean[] types = new boolean[128];
+ for (AudioCodec codec : codecs) {
+ assertFalse(types[codec.type]);
+ types[codec.type] = true;
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/rtp/cts/AudioGroupTest.java b/tests/cts/net/src/android/net/rtp/cts/AudioGroupTest.java
new file mode 100644
index 0000000..fc78e96
--- /dev/null
+++ b/tests/cts/net/src/android/net/rtp/cts/AudioGroupTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.rtp.cts;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.net.rtp.AudioCodec;
+import android.net.rtp.AudioGroup;
+import android.net.rtp.AudioStream;
+import android.net.rtp.RtpStream;
+import android.os.Build;
+import android.platform.test.annotations.AppModeFull;
+import android.test.AndroidTestCase;
+
+import androidx.core.os.BuildCompat;
+
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+
+@AppModeFull(reason = "RtpStream cannot create in instant app mode")
+public class AudioGroupTest extends AndroidTestCase {
+
+ private static final String TAG = AudioGroupTest.class.getSimpleName();
+
+ private AudioManager mAudioManager;
+
+ private AudioStream mStreamA;
+ private DatagramSocket mSocketA;
+ private AudioStream mStreamB;
+ private DatagramSocket mSocketB;
+ private AudioGroup mGroup;
+
+ @Override
+ public void setUp() throws Exception {
+ mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+
+ InetAddress local = InetAddress.getByName("::1");
+
+ mStreamA = new AudioStream(local);
+ mStreamA.setMode(RtpStream.MODE_NORMAL);
+ mStreamA.setCodec(AudioCodec.PCMU);
+ mSocketA = new DatagramSocket();
+ mSocketA.connect(mStreamA.getLocalAddress(), mStreamA.getLocalPort());
+ mStreamA.associate(mSocketA.getLocalAddress(), mSocketA.getLocalPort());
+
+ mStreamB = new AudioStream(local);
+ mStreamB.setMode(RtpStream.MODE_NORMAL);
+ mStreamB.setCodec(AudioCodec.PCMU);
+ mSocketB = new DatagramSocket();
+ mSocketB.connect(mStreamB.getLocalAddress(), mStreamB.getLocalPort());
+ mStreamB.associate(mSocketB.getLocalAddress(), mSocketB.getLocalPort());
+
+ // BuildCompat.isAtLeastR is documented to return false on release SDKs (including R)
+ mGroup = Build.VERSION.SDK_INT > Build.VERSION_CODES.Q || BuildCompat.isAtLeastR()
+ ? new AudioGroup(mContext)
+ : new AudioGroup(); // Constructor with context argument was introduced in R
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mGroup.clear();
+ mStreamA.release();
+ mSocketA.close();
+ mStreamB.release();
+ mSocketB.close();
+ mAudioManager.setMode(AudioManager.MODE_NORMAL);
+ }
+
+ private void assertPacket(DatagramSocket socket, int length) throws Exception {
+ DatagramPacket packet = new DatagramPacket(new byte[length + 1], length + 1);
+ socket.setSoTimeout(3000);
+ socket.receive(packet);
+ assertEquals(packet.getLength(), length);
+ }
+
+ private void drain(DatagramSocket socket) throws Exception {
+ DatagramPacket packet = new DatagramPacket(new byte[1], 1);
+ socket.setSoTimeout(1);
+ try {
+ // Drain the socket by retrieving all the packets queued on it.
+ // A SocketTimeoutException will be thrown when it becomes empty.
+ while (true) {
+ socket.receive(packet);
+ }
+ } catch (Exception e) {
+ // ignore.
+ }
+ }
+
+ public void testTraffic() throws Exception {
+ mStreamA.join(mGroup);
+ assertPacket(mSocketA, 12 + 160);
+
+ mStreamB.join(mGroup);
+ assertPacket(mSocketB, 12 + 160);
+
+ mStreamA.join(null);
+ drain(mSocketA);
+
+ drain(mSocketB);
+ assertPacket(mSocketB, 12 + 160);
+
+ mStreamA.join(mGroup);
+ assertPacket(mSocketA, 12 + 160);
+ }
+
+ public void testSetMode() throws Exception {
+ mGroup.setMode(AudioGroup.MODE_NORMAL);
+ assertEquals(mGroup.getMode(), AudioGroup.MODE_NORMAL);
+
+ mGroup.setMode(AudioGroup.MODE_MUTED);
+ assertEquals(mGroup.getMode(), AudioGroup.MODE_MUTED);
+
+ mStreamA.join(mGroup);
+ mStreamB.join(mGroup);
+
+ mGroup.setMode(AudioGroup.MODE_NORMAL);
+ assertEquals(mGroup.getMode(), AudioGroup.MODE_NORMAL);
+
+ mGroup.setMode(AudioGroup.MODE_MUTED);
+ assertEquals(mGroup.getMode(), AudioGroup.MODE_MUTED);
+ }
+
+ public void testAdd() throws Exception {
+ mStreamA.join(mGroup);
+ assertEquals(mGroup.getStreams().length, 1);
+
+ mStreamB.join(mGroup);
+ assertEquals(mGroup.getStreams().length, 2);
+
+ mStreamA.join(mGroup);
+ assertEquals(mGroup.getStreams().length, 2);
+ }
+
+ public void testRemove() throws Exception {
+ mStreamA.join(mGroup);
+ assertEquals(mGroup.getStreams().length, 1);
+
+ mStreamA.join(null);
+ assertEquals(mGroup.getStreams().length, 0);
+
+ mStreamA.join(mGroup);
+ assertEquals(mGroup.getStreams().length, 1);
+ }
+
+ public void testClear() throws Exception {
+ mStreamA.join(mGroup);
+ mStreamB.join(mGroup);
+ mGroup.clear();
+
+ assertEquals(mGroup.getStreams().length, 0);
+ assertFalse(mStreamA.isBusy());
+ assertFalse(mStreamB.isBusy());
+ }
+
+ public void testDoubleClear() throws Exception {
+ mStreamA.join(mGroup);
+ mStreamB.join(mGroup);
+ mGroup.clear();
+ mGroup.clear();
+ }
+}
diff --git a/tests/cts/net/src/android/net/rtp/cts/AudioStreamTest.java b/tests/cts/net/src/android/net/rtp/cts/AudioStreamTest.java
new file mode 100644
index 0000000..f2db6ee
--- /dev/null
+++ b/tests/cts/net/src/android/net/rtp/cts/AudioStreamTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.rtp.cts;
+
+import android.net.rtp.AudioCodec;
+import android.net.rtp.AudioStream;
+import android.platform.test.annotations.AppModeFull;
+import android.test.AndroidTestCase;
+
+import java.net.InetAddress;
+
+@AppModeFull(reason = "RtpStream cannot create in instant app mode")
+public class AudioStreamTest extends AndroidTestCase {
+
+ private void testRtpStream(InetAddress address) throws Exception {
+ AudioStream stream = new AudioStream(address);
+ assertEquals(stream.getLocalAddress(), address);
+ assertEquals(stream.getLocalPort() % 2, 0);
+
+ assertNull(stream.getRemoteAddress());
+ assertEquals(stream.getRemotePort(), -1);
+ stream.associate(address, 1000);
+ assertEquals(stream.getRemoteAddress(), address);
+ assertEquals(stream.getRemotePort(), 1000);
+
+ assertFalse(stream.isBusy());
+ stream.release();
+ }
+
+ public void testV4Stream() throws Exception {
+ testRtpStream(InetAddress.getByName("127.0.0.1"));
+ }
+
+ public void testV6Stream() throws Exception {
+ testRtpStream(InetAddress.getByName("::1"));
+ }
+
+ public void testSetDtmfType() throws Exception {
+ AudioStream stream = new AudioStream(InetAddress.getByName("::1"));
+
+ assertEquals(stream.getDtmfType(), -1);
+ try {
+ stream.setDtmfType(0);
+ fail("Expecting IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+ stream.setDtmfType(96);
+ assertEquals(stream.getDtmfType(), 96);
+
+ stream.setCodec(AudioCodec.getCodec(97, "PCMU/8000", null));
+ try {
+ stream.setDtmfType(97);
+ fail("Expecting IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+ stream.release();
+ }
+
+ public void testSetCodec() throws Exception {
+ AudioStream stream = new AudioStream(InetAddress.getByName("::1"));
+
+ assertNull(stream.getCodec());
+ stream.setCodec(AudioCodec.getCodec(97, "PCMU/8000", null));
+ assertNotNull(stream.getCodec());
+
+ stream.setDtmfType(96);
+ try {
+ stream.setCodec(AudioCodec.getCodec(96, "PCMU/8000", null));
+ fail("Expecting IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+ stream.release();
+ }
+
+ public void testDoubleRelease() throws Exception {
+ AudioStream stream = new AudioStream(InetAddress.getByName("::1"));
+ stream.release();
+ stream.release();
+ }
+}
diff --git a/tests/cts/net/util/Android.bp b/tests/cts/net/util/Android.bp
new file mode 100644
index 0000000..c36d976
--- /dev/null
+++ b/tests/cts/net/util/Android.bp
@@ -0,0 +1,26 @@
+//
+// Copyright (C) 2019 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.
+//
+
+// Common utilities for cts net tests.
+java_library {
+ name: "cts-net-utils",
+ srcs: ["java/**/*.java", "java/**/*.kt"],
+ static_libs: [
+ "compatibility-device-util-axt",
+ "junit",
+ "net-tests-utils",
+ ],
+}
\ No newline at end of file
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
new file mode 100644
index 0000000..be0daae
--- /dev/null
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -0,0 +1,693 @@
+/*
+ * Copyright (C) 2019 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.util;
+
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.annotation.NonNull;
+import android.app.AppOpsManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.State;
+import android.net.NetworkRequest;
+import android.net.TestNetworkManager;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import junit.framework.AssertionFailedError;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public final class CtsNetUtils {
+ private static final String TAG = CtsNetUtils.class.getSimpleName();
+ private static final int DURATION = 10000;
+ private static final int SOCKET_TIMEOUT_MS = 2000;
+ private static final int PRIVATE_DNS_PROBE_MS = 1_000;
+
+ private static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 6_000;
+ private static final int CONNECTIVITY_CHANGE_TIMEOUT_SECS = 30;
+ public static final int HTTP_PORT = 80;
+ public static final String TEST_HOST = "connectivitycheck.gstatic.com";
+ public static final String HTTP_REQUEST =
+ "GET /generate_204 HTTP/1.0\r\n" +
+ "Host: " + TEST_HOST + "\r\n" +
+ "Connection: keep-alive\r\n\r\n";
+ // Action sent to ConnectivityActionReceiver when a network callback is sent via PendingIntent.
+ public static final String NETWORK_CALLBACK_ACTION =
+ "ConnectivityManagerTest.NetworkCallbackAction";
+
+ private final IBinder mBinder = new Binder();
+ private final Context mContext;
+ private final ConnectivityManager mCm;
+ private final ContentResolver mCR;
+ private final WifiManager mWifiManager;
+ private TestNetworkCallback mCellNetworkCallback;
+ private String mOldPrivateDnsMode;
+ private String mOldPrivateDnsSpecifier;
+
+ public CtsNetUtils(Context context) {
+ mContext = context;
+ mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ mCR = context.getContentResolver();
+ }
+
+ /** Checks if FEATURE_IPSEC_TUNNELS is enabled on the device */
+ public boolean hasIpsecTunnelsFeature() {
+ return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
+ || SystemProperties.getInt("ro.product.first_api_level", 0)
+ >= Build.VERSION_CODES.Q;
+ }
+
+ /**
+ * Sets the given appop using shell commands
+ *
+ * <p>Expects caller to hold the shell permission identity.
+ */
+ public void setAppopPrivileged(int appop, boolean allow) {
+ final String opName = AppOpsManager.opToName(appop);
+ for (final String pkg : new String[] {"com.android.shell", mContext.getPackageName()}) {
+ final String cmd =
+ String.format(
+ "appops set %s %s %s",
+ pkg, // Package name
+ opName, // Appop
+ (allow ? "allow" : "deny")); // Action
+ SystemUtil.runShellCommand(cmd);
+ }
+ }
+
+ /** Sets up a test network using the provided interface name */
+ public TestNetworkCallback setupAndGetTestNetwork(String ifname) throws Exception {
+ // Build a network request
+ final NetworkRequest nr =
+ new NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(ifname)
+ .build();
+
+ final TestNetworkCallback cb = new TestNetworkCallback();
+ mCm.requestNetwork(nr, cb);
+
+ // Setup the test network after network request is filed to prevent Network from being
+ // reaped due to no requests matching it.
+ mContext.getSystemService(TestNetworkManager.class).setupTestNetwork(ifname, mBinder);
+
+ return cb;
+ }
+
+ // Toggle WiFi twice, leaving it in the state it started in
+ public void toggleWifi() {
+ if (mWifiManager.isWifiEnabled()) {
+ Network wifiNetwork = getWifiNetwork();
+ disconnectFromWifi(wifiNetwork);
+ connectToWifi();
+ } else {
+ connectToWifi();
+ Network wifiNetwork = getWifiNetwork();
+ disconnectFromWifi(wifiNetwork);
+ }
+ }
+
+ /**
+ * Enable WiFi and wait for it to become connected to a network.
+ *
+ * This method expects to receive a legacy broadcast on connect, which may not be sent if the
+ * network does not become default or if it is not the first network.
+ */
+ public Network connectToWifi() {
+ return connectToWifi(true /* expectLegacyBroadcast */);
+ }
+
+ /**
+ * Enable WiFi and wait for it to become connected to a network.
+ *
+ * A network is considered connected when a {@link NetworkRequest} with TRANSPORT_WIFI
+ * receives a {@link NetworkCallback#onAvailable(Network)} callback.
+ */
+ public Network ensureWifiConnected() {
+ return connectToWifi(false /* expectLegacyBroadcast */);
+ }
+
+ /**
+ * Enable WiFi and wait for it to become connected to a network.
+ *
+ * @param expectLegacyBroadcast Whether to check for a legacy CONNECTIVITY_ACTION connected
+ * broadcast. The broadcast is typically not sent if the network
+ * does not become the default network, and is not the first
+ * network to appear.
+ * @return The network that was newly connected.
+ */
+ private Network connectToWifi(boolean expectLegacyBroadcast) {
+ final TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback);
+ Network wifiNetwork = null;
+
+ ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
+ mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ mContext.registerReceiver(receiver, filter);
+
+ boolean connected = false;
+ final String err = "Wifi must be configured to connect to an access point for this test.";
+ try {
+ clearWifiBlacklist();
+ SystemUtil.runShellCommand("svc wifi enable");
+ final WifiConfiguration config = maybeAddVirtualWifiConfiguration();
+ if (config == null) {
+ // TODO: this may not clear the BSSID blacklist, as opposed to
+ // mWifiManager.connect(config)
+ assertTrue("Error reconnecting wifi", runAsShell(NETWORK_SETTINGS,
+ mWifiManager::reconnect));
+ } else {
+ // When running CTS, devices are expected to have wifi networks pre-configured.
+ // This condition is only hit on virtual devices.
+ final Integer error = runAsShell(NETWORK_SETTINGS, () -> {
+ final ConnectWifiListener listener = new ConnectWifiListener();
+ mWifiManager.connect(config, listener);
+ return listener.connectFuture.get(
+ CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS);
+ });
+ assertNull("Error connecting to wifi: " + error, error);
+ }
+ // Ensure we get an onAvailable callback and possibly a CONNECTIVITY_ACTION.
+ wifiNetwork = callback.waitForAvailable();
+ assertNotNull(err, wifiNetwork);
+ connected = !expectLegacyBroadcast || receiver.waitForState();
+ } catch (InterruptedException ex) {
+ fail("connectToWifi was interrupted");
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ mContext.unregisterReceiver(receiver);
+ }
+
+ assertTrue(err, connected);
+ return wifiNetwork;
+ }
+
+ private static class ConnectWifiListener implements WifiManager.ActionListener {
+ /**
+ * Future completed when the connect process ends. Provides the error code or null if none.
+ */
+ final CompletableFuture<Integer> connectFuture = new CompletableFuture<>();
+ @Override
+ public void onSuccess() {
+ connectFuture.complete(null);
+ }
+
+ @Override
+ public void onFailure(int reason) {
+ connectFuture.complete(reason);
+ }
+ }
+
+ private WifiConfiguration maybeAddVirtualWifiConfiguration() {
+ final List<WifiConfiguration> configs = runAsShell(NETWORK_SETTINGS,
+ mWifiManager::getConfiguredNetworks);
+ // If no network is configured, add a config for virtual access points if applicable
+ if (configs.size() == 0) {
+ final List<ScanResult> scanResults = getWifiScanResults();
+ final WifiConfiguration virtualConfig = maybeConfigureVirtualNetwork(scanResults);
+ assertNotNull("The device has no configured wifi network", virtualConfig);
+
+ return virtualConfig;
+ }
+ // No need to add a configuration: there is already one
+ return null;
+ }
+
+ private List<ScanResult> getWifiScanResults() {
+ final CompletableFuture<List<ScanResult>> scanResultsFuture = new CompletableFuture<>();
+ runAsShell(NETWORK_SETTINGS, () -> {
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ scanResultsFuture.complete(mWifiManager.getScanResults());
+ }
+ };
+ mContext.registerReceiver(receiver, new IntentFilter(SCAN_RESULTS_AVAILABLE_ACTION));
+ mWifiManager.startScan();
+ });
+
+ try {
+ return scanResultsFuture.get(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS);
+ } catch (ExecutionException | InterruptedException | TimeoutException e) {
+ throw new AssertionFailedError("Wifi scan results not received within timeout");
+ }
+ }
+
+ /**
+ * If a virtual wifi network is detected, add a configuration for that network.
+ * TODO(b/158150376): have the test infrastructure add virtual wifi networks when appropriate.
+ */
+ private WifiConfiguration maybeConfigureVirtualNetwork(List<ScanResult> scanResults) {
+ // Virtual wifi networks used on the emulator and cloud testing infrastructure
+ final List<String> virtualSsids = Arrays.asList("VirtWifi", "AndroidWifi");
+ Log.d(TAG, "Wifi scan results: " + scanResults);
+ final ScanResult virtualScanResult = scanResults.stream().filter(
+ s -> virtualSsids.contains(s.SSID)).findFirst().orElse(null);
+
+ // Only add the virtual configuration if the virtual AP is detected in scans
+ if (virtualScanResult == null) return null;
+
+ final WifiConfiguration virtualConfig = new WifiConfiguration();
+ // ASCII SSIDs need to be surrounded by double quotes
+ virtualConfig.SSID = "\"" + virtualScanResult.SSID + "\"";
+ virtualConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+
+ runAsShell(NETWORK_SETTINGS, () -> {
+ final int networkId = mWifiManager.addNetwork(virtualConfig);
+ assertTrue(networkId >= 0);
+ assertTrue(mWifiManager.enableNetwork(networkId, false /* attemptConnect */));
+ });
+ return virtualConfig;
+ }
+
+ /**
+ * Re-enable wifi networks that were blacklisted, typically because no internet connection was
+ * detected the last time they were connected. This is necessary to make sure wifi can reconnect
+ * to them.
+ */
+ private void clearWifiBlacklist() {
+ runAsShell(NETWORK_SETTINGS, () -> {
+ for (WifiConfiguration cfg : mWifiManager.getConfiguredNetworks()) {
+ assertTrue(mWifiManager.enableNetwork(cfg.networkId, false /* attemptConnect */));
+ }
+ });
+ }
+
+ /**
+ * Disable WiFi and wait for it to become disconnected from the network.
+ *
+ * This method expects to receive a legacy broadcast on disconnect, which may not be sent if the
+ * network was not default, or was not the first network.
+ *
+ * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network
+ * is expected to be able to establish a TCP connection to a remote
+ * server before disconnecting, and to have that connection closed in
+ * the process.
+ */
+ public void disconnectFromWifi(Network wifiNetworkToCheck) {
+ disconnectFromWifi(wifiNetworkToCheck, true /* expectLegacyBroadcast */);
+ }
+
+ /**
+ * Disable WiFi and wait for it to become disconnected from the network.
+ *
+ * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network
+ * is expected to be able to establish a TCP connection to a remote
+ * server before disconnecting, and to have that connection closed in
+ * the process.
+ */
+ public void ensureWifiDisconnected(Network wifiNetworkToCheck) {
+ disconnectFromWifi(wifiNetworkToCheck, false /* expectLegacyBroadcast */);
+ }
+
+ /**
+ * Disable WiFi and wait for it to become disconnected from the network.
+ *
+ * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network
+ * is expected to be able to establish a TCP connection to a remote
+ * server before disconnecting, and to have that connection closed in
+ * the process.
+ * @param expectLegacyBroadcast Whether to check for a legacy CONNECTIVITY_ACTION disconnected
+ * broadcast. The broadcast is typically not sent if the network
+ * was not the default network and not the first network to appear.
+ * The check will always be skipped if the device was not connected
+ * to wifi in the first place.
+ */
+ private void disconnectFromWifi(Network wifiNetworkToCheck, boolean expectLegacyBroadcast) {
+ final TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback);
+
+ ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
+ mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.DISCONNECTED);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ mContext.registerReceiver(receiver, filter);
+
+ final WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
+ final boolean wasWifiConnected = wifiInfo != null && wifiInfo.getNetworkId() != -1;
+ // Assert that we can establish a TCP connection on wifi.
+ Socket wifiBoundSocket = null;
+ if (wifiNetworkToCheck != null) {
+ assertTrue("Cannot check network " + wifiNetworkToCheck + ": wifi is not connected",
+ wasWifiConnected);
+ final NetworkCapabilities nc = mCm.getNetworkCapabilities(wifiNetworkToCheck);
+ assertNotNull("Network " + wifiNetworkToCheck + " is not connected", nc);
+ try {
+ wifiBoundSocket = getBoundSocket(wifiNetworkToCheck, TEST_HOST, HTTP_PORT);
+ testHttpRequest(wifiBoundSocket);
+ } catch (IOException e) {
+ fail("HTTP request before wifi disconnected failed with: " + e);
+ }
+ }
+
+ try {
+ SystemUtil.runShellCommand("svc wifi disable");
+ if (wasWifiConnected) {
+ // Ensure we get both an onLost callback and a CONNECTIVITY_ACTION.
+ assertNotNull("Did not receive onLost callback after disabling wifi",
+ callback.waitForLost());
+ }
+ if (wasWifiConnected && expectLegacyBroadcast) {
+ assertTrue("Wifi failed to reach DISCONNECTED state.", receiver.waitForState());
+ }
+ } catch (InterruptedException ex) {
+ fail("disconnectFromWifi was interrupted");
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ mContext.unregisterReceiver(receiver);
+ }
+
+ // Check that the socket is closed when wifi disconnects.
+ if (wifiBoundSocket != null) {
+ try {
+ testHttpRequest(wifiBoundSocket);
+ fail("HTTP request should not succeed after wifi disconnects");
+ } catch (IOException expected) {
+ assertEquals(Os.strerror(OsConstants.ECONNABORTED), expected.getMessage());
+ }
+ }
+ }
+
+ public Network getWifiNetwork() {
+ TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback);
+ Network network = null;
+ try {
+ network = callback.waitForAvailable();
+ } catch (InterruptedException e) {
+ fail("NetworkCallback wait was interrupted.");
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ }
+ assertNotNull("Cannot find Network for wifi. Is wifi connected?", network);
+ return network;
+ }
+
+ public Network connectToCell() throws InterruptedException {
+ if (cellConnectAttempted()) {
+ throw new IllegalStateException("Already connected");
+ }
+ NetworkRequest cellRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build();
+ mCellNetworkCallback = new TestNetworkCallback();
+ mCm.requestNetwork(cellRequest, mCellNetworkCallback);
+ final Network cellNetwork = mCellNetworkCallback.waitForAvailable();
+ assertNotNull("Cell network not available. " +
+ "Please ensure the device has working mobile data.", cellNetwork);
+ return cellNetwork;
+ }
+
+ public void disconnectFromCell() {
+ if (!cellConnectAttempted()) {
+ throw new IllegalStateException("Cell connection not attempted");
+ }
+ mCm.unregisterNetworkCallback(mCellNetworkCallback);
+ mCellNetworkCallback = null;
+ }
+
+ public boolean cellConnectAttempted() {
+ return mCellNetworkCallback != null;
+ }
+
+ private NetworkRequest makeWifiNetworkRequest() {
+ return new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .build();
+ }
+
+ private void testHttpRequest(Socket s) throws IOException {
+ OutputStream out = s.getOutputStream();
+ InputStream in = s.getInputStream();
+
+ final byte[] requestBytes = HTTP_REQUEST.getBytes("UTF-8");
+ byte[] responseBytes = new byte[4096];
+ out.write(requestBytes);
+ in.read(responseBytes);
+ assertTrue(new String(responseBytes, "UTF-8").startsWith("HTTP/1.0 204 No Content\r\n"));
+ }
+
+ private Socket getBoundSocket(Network network, String host, int port) throws IOException {
+ InetSocketAddress addr = new InetSocketAddress(host, port);
+ Socket s = network.getSocketFactory().createSocket();
+ try {
+ s.setSoTimeout(SOCKET_TIMEOUT_MS);
+ s.connect(addr, SOCKET_TIMEOUT_MS);
+ } catch (IOException e) {
+ s.close();
+ throw e;
+ }
+ return s;
+ }
+
+ public void storePrivateDnsSetting() {
+ // Store private DNS setting
+ mOldPrivateDnsMode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
+ mOldPrivateDnsSpecifier = Settings.Global.getString(mCR,
+ Settings.Global.PRIVATE_DNS_SPECIFIER);
+ // It's possible that there is no private DNS default value in Settings.
+ // Give it a proper default mode which is opportunistic mode.
+ if (mOldPrivateDnsMode == null) {
+ mOldPrivateDnsSpecifier = "";
+ mOldPrivateDnsMode = PRIVATE_DNS_MODE_OPPORTUNISTIC;
+ Settings.Global.putString(mCR,
+ Settings.Global.PRIVATE_DNS_SPECIFIER, mOldPrivateDnsSpecifier);
+ Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldPrivateDnsMode);
+ }
+ }
+
+ public void restorePrivateDnsSetting() throws InterruptedException {
+ if (mOldPrivateDnsMode == null || mOldPrivateDnsSpecifier == null) {
+ return;
+ }
+ // restore private DNS setting
+ if ("hostname".equals(mOldPrivateDnsMode)) {
+ setPrivateDnsStrictMode(mOldPrivateDnsSpecifier);
+ awaitPrivateDnsSetting("restorePrivateDnsSetting timeout",
+ mCm.getActiveNetwork(),
+ mOldPrivateDnsSpecifier, true);
+ } else {
+ Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldPrivateDnsMode);
+ }
+ }
+
+ public void setPrivateDnsStrictMode(String server) {
+ // To reduce flake rate, set PRIVATE_DNS_SPECIFIER before PRIVATE_DNS_MODE. This ensures
+ // that if the previous private DNS mode was not "hostname", the system only sees one
+ // EVENT_PRIVATE_DNS_SETTINGS_CHANGED event instead of two.
+ Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_SPECIFIER, server);
+ final String mode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
+ // If current private DNS mode is "hostname", we only need to set PRIVATE_DNS_SPECIFIER.
+ if (!"hostname".equals(mode)) {
+ Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, "hostname");
+ }
+ }
+
+ public void awaitPrivateDnsSetting(@NonNull String msg, @NonNull Network network,
+ @NonNull String server, boolean requiresValidatedServers) throws InterruptedException {
+ CountDownLatch latch = new CountDownLatch(1);
+ NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
+ NetworkCallback callback = new NetworkCallback() {
+ @Override
+ public void onLinkPropertiesChanged(Network n, LinkProperties lp) {
+ if (requiresValidatedServers && lp.getValidatedPrivateDnsServers().isEmpty()) {
+ return;
+ }
+ if (network.equals(n) && server.equals(lp.getPrivateDnsServerName())) {
+ latch.countDown();
+ }
+ }
+ };
+ mCm.registerNetworkCallback(request, callback);
+ assertTrue(msg, latch.await(PRIVATE_DNS_SETTING_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ mCm.unregisterNetworkCallback(callback);
+ // Wait some time for NetworkMonitor's private DNS probe to complete. If we do not do
+ // this, then the test could complete before the NetworkMonitor private DNS probe
+ // completes. This would result in tearDown disabling private DNS, and the NetworkMonitor
+ // private DNS probe getting stuck because there are no longer any private DNS servers to
+ // query. This then results in the next test not being able to change the private DNS
+ // setting within the timeout, because the NetworkMonitor thread is blocked in the
+ // private DNS probe. There is no way to know when the probe has completed: because the
+ // network is likely already validated, there is no callback that we can listen to, so
+ // just sleep.
+ if (requiresValidatedServers) {
+ Thread.sleep(PRIVATE_DNS_PROBE_MS);
+ }
+ }
+
+ /**
+ * Receiver that captures the last connectivity change's network type and state. Recognizes
+ * both {@code CONNECTIVITY_ACTION} and {@code NETWORK_CALLBACK_ACTION} intents.
+ */
+ public static class ConnectivityActionReceiver extends BroadcastReceiver {
+
+ private final CountDownLatch mReceiveLatch = new CountDownLatch(1);
+
+ private final int mNetworkType;
+ private final NetworkInfo.State mNetState;
+ private final ConnectivityManager mCm;
+
+ public ConnectivityActionReceiver(ConnectivityManager cm, int networkType,
+ NetworkInfo.State netState) {
+ this.mCm = cm;
+ mNetworkType = networkType;
+ mNetState = netState;
+ }
+
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ NetworkInfo networkInfo = null;
+
+ // When receiving ConnectivityManager.CONNECTIVITY_ACTION, the NetworkInfo parcelable
+ // is stored in EXTRA_NETWORK_INFO. With a NETWORK_CALLBACK_ACTION, the Network is
+ // sent in EXTRA_NETWORK and we need to ask the ConnectivityManager for the NetworkInfo.
+ if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
+ networkInfo = intent.getExtras()
+ .getParcelable(ConnectivityManager.EXTRA_NETWORK_INFO);
+ assertNotNull("ConnectivityActionReceiver expected EXTRA_NETWORK_INFO",
+ networkInfo);
+ } else if (NETWORK_CALLBACK_ACTION.equals(action)) {
+ Network network = intent.getExtras()
+ .getParcelable(ConnectivityManager.EXTRA_NETWORK);
+ assertNotNull("ConnectivityActionReceiver expected EXTRA_NETWORK", network);
+ networkInfo = this.mCm.getNetworkInfo(network);
+ if (networkInfo == null) {
+ // When disconnecting, it seems like we get an intent sent with an invalid
+ // Network; that is, by the time we call ConnectivityManager.getNetworkInfo(),
+ // it is invalid. Ignore these.
+ Log.i(TAG, "ConnectivityActionReceiver NETWORK_CALLBACK_ACTION ignoring "
+ + "invalid network");
+ return;
+ }
+ } else {
+ fail("ConnectivityActionReceiver received unxpected intent action: " + action);
+ }
+
+ assertNotNull("ConnectivityActionReceiver didn't find NetworkInfo", networkInfo);
+ int networkType = networkInfo.getType();
+ State networkState = networkInfo.getState();
+ Log.i(TAG, "Network type: " + networkType + " state: " + networkState);
+ if (networkType == mNetworkType && networkInfo.getState() == mNetState) {
+ mReceiveLatch.countDown();
+ }
+ }
+
+ public boolean waitForState() throws InterruptedException {
+ return mReceiveLatch.await(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Callback used in testRegisterNetworkCallback that allows caller to block on
+ * {@code onAvailable}.
+ */
+ public static class TestNetworkCallback extends ConnectivityManager.NetworkCallback {
+ private final CountDownLatch mAvailableLatch = new CountDownLatch(1);
+ private final CountDownLatch mLostLatch = new CountDownLatch(1);
+ private final CountDownLatch mUnavailableLatch = new CountDownLatch(1);
+
+ public Network currentNetwork;
+ public Network lastLostNetwork;
+
+ public Network waitForAvailable() throws InterruptedException {
+ return mAvailableLatch.await(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS)
+ ? currentNetwork : null;
+ }
+
+ public Network waitForLost() throws InterruptedException {
+ return mLostLatch.await(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS)
+ ? lastLostNetwork : null;
+ }
+
+ public boolean waitForUnavailable() throws InterruptedException {
+ return mUnavailableLatch.await(2, TimeUnit.SECONDS);
+ }
+
+
+ @Override
+ public void onAvailable(Network network) {
+ currentNetwork = network;
+ mAvailableLatch.countDown();
+ }
+
+ @Override
+ public void onLost(Network network) {
+ lastLostNetwork = network;
+ if (network.equals(currentNetwork)) {
+ currentNetwork = null;
+ }
+ mLostLatch.countDown();
+ }
+
+ @Override
+ public void onUnavailable() {
+ mUnavailableLatch.countDown();
+ }
+ }
+}
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
new file mode 100644
index 0000000..c95dc28
--- /dev/null
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright (C) 2020 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.util;
+
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.Network;
+import android.net.TetheredClient;
+import android.net.TetheringManager;
+import android.net.TetheringManager.TetheringEventCallback;
+import android.net.TetheringManager.TetheringInterfaceRegexps;
+import android.net.TetheringManager.TetheringRequest;
+import android.net.wifi.WifiClient;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.SoftApCallback;
+import android.os.ConditionVariable;
+
+import androidx.annotation.NonNull;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.net.module.util.ArrayTrackRecord;
+
+import java.util.Collection;
+import java.util.List;
+
+public final class CtsTetheringUtils {
+ private TetheringManager mTm;
+ private WifiManager mWm;
+ private Context mContext;
+
+ private static final int DEFAULT_TIMEOUT_MS = 60_000;
+
+ public CtsTetheringUtils(Context ctx) {
+ mContext = ctx;
+ mTm = mContext.getSystemService(TetheringManager.class);
+ mWm = mContext.getSystemService(WifiManager.class);
+ }
+
+ public static class StartTetheringCallback implements TetheringManager.StartTetheringCallback {
+ private static int TIMEOUT_MS = 30_000;
+ public static class CallbackValue {
+ public final int error;
+
+ private CallbackValue(final int e) {
+ error = e;
+ }
+
+ public static class OnTetheringStarted extends CallbackValue {
+ OnTetheringStarted() { super(TETHER_ERROR_NO_ERROR); }
+ }
+
+ public static class OnTetheringFailed extends CallbackValue {
+ OnTetheringFailed(final int error) { super(error); }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s(%d)", getClass().getSimpleName(), error);
+ }
+ }
+
+ private final ArrayTrackRecord<CallbackValue>.ReadHead mHistory =
+ new ArrayTrackRecord<CallbackValue>().newReadHead();
+
+ @Override
+ public void onTetheringStarted() {
+ mHistory.add(new CallbackValue.OnTetheringStarted());
+ }
+
+ @Override
+ public void onTetheringFailed(final int error) {
+ mHistory.add(new CallbackValue.OnTetheringFailed(error));
+ }
+
+ public void verifyTetheringStarted() {
+ final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+ assertNotNull("No onTetheringStarted after " + TIMEOUT_MS + " ms", cv);
+ assertTrue("Fail start tethering:" + cv,
+ cv instanceof CallbackValue.OnTetheringStarted);
+ }
+
+ public void expectTetheringFailed(final int expected) throws InterruptedException {
+ final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+ assertNotNull("No onTetheringFailed after " + TIMEOUT_MS + " ms", cv);
+ assertTrue("Expect fail with error code " + expected + ", but received: " + cv,
+ (cv instanceof CallbackValue.OnTetheringFailed) && (cv.error == expected));
+ }
+ }
+
+ public static boolean isIfaceMatch(final List<String> ifaceRegexs, final List<String> ifaces) {
+ return isIfaceMatch(ifaceRegexs.toArray(new String[0]), ifaces);
+ }
+
+ public static boolean isIfaceMatch(final String[] ifaceRegexs, final List<String> ifaces) {
+ if (ifaceRegexs == null) fail("ifaceRegexs should not be null");
+
+ if (ifaces == null) return false;
+
+ for (String s : ifaces) {
+ for (String regex : ifaceRegexs) {
+ if (s.matches(regex)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ // Must poll the callback before looking at the member.
+ public static class TestTetheringEventCallback implements TetheringEventCallback {
+ private static final int TIMEOUT_MS = 30_000;
+
+ public enum CallbackType {
+ ON_SUPPORTED,
+ ON_UPSTREAM,
+ ON_TETHERABLE_REGEX,
+ ON_TETHERABLE_IFACES,
+ ON_TETHERED_IFACES,
+ ON_ERROR,
+ ON_CLIENTS,
+ ON_OFFLOAD_STATUS,
+ };
+
+ public static class CallbackValue {
+ public final CallbackType callbackType;
+ public final Object callbackParam;
+ public final int callbackParam2;
+
+ private CallbackValue(final CallbackType type, final Object param, final int param2) {
+ this.callbackType = type;
+ this.callbackParam = param;
+ this.callbackParam2 = param2;
+ }
+ }
+
+ private final ArrayTrackRecord<CallbackValue> mHistory =
+ new ArrayTrackRecord<CallbackValue>();
+
+ private final ArrayTrackRecord<CallbackValue>.ReadHead mCurrent =
+ mHistory.newReadHead();
+
+ private TetheringInterfaceRegexps mTetherableRegex;
+ private List<String> mTetherableIfaces;
+ private List<String> mTetheredIfaces;
+
+ @Override
+ public void onTetheringSupported(boolean supported) {
+ mHistory.add(new CallbackValue(CallbackType.ON_SUPPORTED, null, (supported ? 1 : 0)));
+ }
+
+ @Override
+ public void onUpstreamChanged(Network network) {
+ mHistory.add(new CallbackValue(CallbackType.ON_UPSTREAM, network, 0));
+ }
+
+ @Override
+ public void onTetherableInterfaceRegexpsChanged(TetheringInterfaceRegexps reg) {
+ mTetherableRegex = reg;
+ mHistory.add(new CallbackValue(CallbackType.ON_TETHERABLE_REGEX, reg, 0));
+ }
+
+ @Override
+ public void onTetherableInterfacesChanged(List<String> interfaces) {
+ mTetherableIfaces = interfaces;
+ mHistory.add(new CallbackValue(CallbackType.ON_TETHERABLE_IFACES, interfaces, 0));
+ }
+
+ @Override
+ public void onTetheredInterfacesChanged(List<String> interfaces) {
+ mTetheredIfaces = interfaces;
+ mHistory.add(new CallbackValue(CallbackType.ON_TETHERED_IFACES, interfaces, 0));
+ }
+
+ @Override
+ public void onError(String ifName, int error) {
+ mHistory.add(new CallbackValue(CallbackType.ON_ERROR, ifName, error));
+ }
+
+ @Override
+ public void onClientsChanged(Collection<TetheredClient> clients) {
+ mHistory.add(new CallbackValue(CallbackType.ON_CLIENTS, clients, 0));
+ }
+
+ @Override
+ public void onOffloadStatusChanged(int status) {
+ mHistory.add(new CallbackValue(CallbackType.ON_OFFLOAD_STATUS, status, 0));
+ }
+
+ public void expectTetherableInterfacesChanged(@NonNull List<String> regexs) {
+ assertNotNull("No expected tetherable ifaces callback", mCurrent.poll(TIMEOUT_MS,
+ (cv) -> {
+ if (cv.callbackType != CallbackType.ON_TETHERABLE_IFACES) return false;
+ final List<String> interfaces = (List<String>) cv.callbackParam;
+ return isIfaceMatch(regexs, interfaces);
+ }));
+ }
+
+ public void expectTetheredInterfacesChanged(@NonNull List<String> regexs) {
+ assertNotNull("No expected tethered ifaces callback", mCurrent.poll(TIMEOUT_MS,
+ (cv) -> {
+ if (cv.callbackType != CallbackType.ON_TETHERED_IFACES) return false;
+
+ final List<String> interfaces = (List<String>) cv.callbackParam;
+
+ // Null regexs means no active tethering.
+ if (regexs == null) return interfaces.isEmpty();
+
+ return isIfaceMatch(regexs, interfaces);
+ }));
+ }
+
+ public void expectCallbackStarted() {
+ int receivedBitMap = 0;
+ // The each bit represent a type from CallbackType.ON_*.
+ // Expect all of callbacks except for ON_ERROR.
+ final int expectedBitMap = 0xff ^ (1 << CallbackType.ON_ERROR.ordinal());
+ // Receive ON_ERROR on started callback is not matter. It just means tethering is
+ // failed last time, should able to continue the test this time.
+ while ((receivedBitMap & expectedBitMap) != expectedBitMap) {
+ final CallbackValue cv = mCurrent.poll(TIMEOUT_MS, c -> true);
+ if (cv == null) {
+ fail("No expected callbacks, " + "expected bitmap: "
+ + expectedBitMap + ", actual: " + receivedBitMap);
+ }
+
+ receivedBitMap |= (1 << cv.callbackType.ordinal());
+ }
+ }
+
+ public void expectOneOfOffloadStatusChanged(int... offloadStatuses) {
+ assertNotNull("No offload status changed", mCurrent.poll(TIMEOUT_MS, (cv) -> {
+ if (cv.callbackType != CallbackType.ON_OFFLOAD_STATUS) return false;
+
+ final int status = (int) cv.callbackParam;
+ for (int offloadStatus : offloadStatuses) {
+ if (offloadStatus == status) return true;
+ }
+
+ return false;
+ }));
+ }
+
+ public void expectErrorOrTethered(final String iface) {
+ assertNotNull("No expected callback", mCurrent.poll(TIMEOUT_MS, (cv) -> {
+ if (cv.callbackType == CallbackType.ON_ERROR
+ && iface.equals((String) cv.callbackParam)) {
+ return true;
+ }
+ if (cv.callbackType == CallbackType.ON_TETHERED_IFACES
+ && ((List<String>) cv.callbackParam).contains(iface)) {
+ return true;
+ }
+
+ return false;
+ }));
+ }
+
+ public Network getCurrentValidUpstream() {
+ final CallbackValue result = mCurrent.poll(TIMEOUT_MS, (cv) -> {
+ return (cv.callbackType == CallbackType.ON_UPSTREAM)
+ && cv.callbackParam != null;
+ });
+
+ assertNotNull("No valid upstream", result);
+ return (Network) result.callbackParam;
+ }
+
+ public void assumeTetheringSupported() {
+ final ArrayTrackRecord<CallbackValue>.ReadHead history =
+ mHistory.newReadHead();
+ assertNotNull("No onSupported callback", history.poll(TIMEOUT_MS, (cv) -> {
+ if (cv.callbackType != CallbackType.ON_SUPPORTED) return false;
+
+ assumeTrue(cv.callbackParam2 == 1 /* supported */);
+ return true;
+ }));
+ }
+
+ public void assumeWifiTetheringSupported(final Context ctx) throws Exception {
+ assumeTetheringSupported();
+
+ assumeTrue(!getTetheringInterfaceRegexps().getTetherableWifiRegexs().isEmpty());
+
+ final PackageManager pm = ctx.getPackageManager();
+ assumeTrue(pm.hasSystemFeature(PackageManager.FEATURE_WIFI));
+
+ WifiManager wm = ctx.getSystemService(WifiManager.class);
+ // Wifi feature flags only work when wifi is on.
+ final boolean previousWifiEnabledState = wm.isWifiEnabled();
+ try {
+ if (!previousWifiEnabledState) SystemUtil.runShellCommand("svc wifi enable");
+ waitForWifiEnabled(ctx);
+ assumeTrue(wm.isPortableHotspotSupported());
+ } finally {
+ if (!previousWifiEnabledState) SystemUtil.runShellCommand("svc wifi disable");
+ }
+ }
+
+ public TetheringInterfaceRegexps getTetheringInterfaceRegexps() {
+ return mTetherableRegex;
+ }
+
+ public List<String> getTetherableInterfaces() {
+ return mTetherableIfaces;
+ }
+
+ public List<String> getTetheredInterfaces() {
+ return mTetheredIfaces;
+ }
+ }
+
+ private static void waitForWifiEnabled(final Context ctx) throws Exception {
+ WifiManager wm = ctx.getSystemService(WifiManager.class);
+ if (wm.isWifiEnabled()) return;
+
+ final ConditionVariable mWaiting = new ConditionVariable();
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
+ if (wm.isWifiEnabled()) mWaiting.open();
+ }
+ }
+ };
+ try {
+ ctx.registerReceiver(receiver, new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION));
+ if (!mWaiting.block(DEFAULT_TIMEOUT_MS)) {
+ assertTrue("Wifi did not become enabled after " + DEFAULT_TIMEOUT_MS + "ms",
+ wm.isWifiEnabled());
+ }
+ } finally {
+ ctx.unregisterReceiver(receiver);
+ }
+ }
+
+ public TestTetheringEventCallback registerTetheringEventCallback() {
+ final TestTetheringEventCallback tetherEventCallback =
+ new TestTetheringEventCallback();
+
+ mTm.registerTetheringEventCallback(c -> c.run() /* executor */, tetherEventCallback);
+ tetherEventCallback.expectCallbackStarted();
+
+ return tetherEventCallback;
+ }
+
+ public void unregisterTetheringEventCallback(final TestTetheringEventCallback callback) {
+ mTm.unregisterTetheringEventCallback(callback);
+ }
+
+ private static List<String> getWifiTetherableInterfaceRegexps(
+ final TestTetheringEventCallback callback) {
+ return callback.getTetheringInterfaceRegexps().getTetherableWifiRegexs();
+ }
+
+ public static boolean isWifiTetheringSupported(final TestTetheringEventCallback callback) {
+ return !getWifiTetherableInterfaceRegexps(callback).isEmpty();
+ }
+
+ public void startWifiTethering(final TestTetheringEventCallback callback)
+ throws InterruptedException {
+ final List<String> wifiRegexs = getWifiTetherableInterfaceRegexps(callback);
+ assertFalse(isIfaceMatch(wifiRegexs, callback.getTetheredInterfaces()));
+
+ final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
+ final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setShouldShowEntitlementUi(false).build();
+ mTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
+ startTetheringCallback.verifyTetheringStarted();
+
+ callback.expectTetheredInterfacesChanged(wifiRegexs);
+
+ callback.expectOneOfOffloadStatusChanged(
+ TETHER_HARDWARE_OFFLOAD_STARTED,
+ TETHER_HARDWARE_OFFLOAD_FAILED);
+ }
+
+ private static class StopSoftApCallback implements SoftApCallback {
+ private final ConditionVariable mWaiting = new ConditionVariable();
+ @Override
+ public void onStateChanged(int state, int failureReason) {
+ if (state == WifiManager.WIFI_AP_STATE_DISABLED) mWaiting.open();
+ }
+
+ @Override
+ public void onConnectedClientsChanged(List<WifiClient> clients) { }
+
+ public void waitForSoftApStopped() {
+ if (!mWaiting.block(DEFAULT_TIMEOUT_MS)) {
+ fail("stopSoftAp Timeout");
+ }
+ }
+ }
+
+ // Wait for softAp to be disabled. This is necessary on devices where stopping softAp
+ // deletes the interface. On these devices, tethering immediately stops when the softAp
+ // interface is removed, but softAp is not yet fully disabled. Wait for softAp to be
+ // fully disabled, because otherwise the next test might fail because it attempts to
+ // start softAp before it's fully stopped.
+ public void expectSoftApDisabled() {
+ final StopSoftApCallback callback = new StopSoftApCallback();
+ try {
+ mWm.registerSoftApCallback(c -> c.run(), callback);
+ // registerSoftApCallback will immediately call the callback with the current state, so
+ // this callback will fire even if softAp is already disabled.
+ callback.waitForSoftApStopped();
+ } finally {
+ mWm.unregisterSoftApCallback(callback);
+ }
+ }
+
+ public void stopWifiTethering(final TestTetheringEventCallback callback) {
+ mTm.stopTethering(TETHERING_WIFI);
+ expectSoftApDisabled();
+ callback.expectTetheredInterfacesChanged(null);
+ callback.expectOneOfOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
+ }
+}
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
new file mode 100644
index 0000000..b1d4a60
--- /dev/null
+++ b/tests/cts/tethering/Android.bp
@@ -0,0 +1,56 @@
+// Copyright (C) 2019 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.
+
+android_test {
+ name: "CtsTetheringTest",
+ defaults: ["cts_defaults"],
+
+ libs: [
+ "android.test.base",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ ],
+
+ static_libs: [
+ "TetheringCommonTests",
+ "TetheringIntegrationTestsLib",
+ "compatibility-device-util-axt",
+ "cts-net-utils",
+ "net-tests-utils",
+ "ctstestrunner-axt",
+ "junit",
+ "junit-params",
+ ],
+
+ jni_libs: [
+ // For mockito extended
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+
+ // Change to system current when TetheringManager move to bootclass path.
+ platform_apis: true,
+
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ "mts",
+ ],
+
+ // Include both the 32 and 64 bit versions
+ compile_multilib: "both",
+}
diff --git a/tests/cts/tethering/AndroidManifest.xml b/tests/cts/tethering/AndroidManifest.xml
new file mode 100644
index 0000000..911dbf2
--- /dev/null
+++ b/tests/cts/tethering/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2019 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.tethering.cts">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.tethering.cts"
+ android:label="CTS tests of android.tethering">
+ <meta-data android:name="listener"
+ android:value="com.android.cts.runner.CtsTestRunListener" />
+ </instrumentation>
+
+</manifest>
diff --git a/tests/cts/tethering/AndroidTest.xml b/tests/cts/tethering/AndroidTest.xml
new file mode 100644
index 0000000..e752e3a
--- /dev/null
+++ b/tests/cts/tethering/AndroidTest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<configuration description="Config for CTS Tethering test cases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="networking" />
+ <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <option name="not-shardable" value="true" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="CtsTetheringTest.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.tethering.cts" />
+ </test>
+
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+</configuration>
diff --git a/tests/cts/tethering/OWNERS b/tests/cts/tethering/OWNERS
new file mode 100644
index 0000000..cd6abeb
--- /dev/null
+++ b/tests/cts/tethering/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 31808
+lorenzo@google.com
+satk@google.com
+
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
new file mode 100644
index 0000000..71a81ff
--- /dev/null
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2019 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.tethering.test;
+
+import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.cts.util.CtsTetheringUtils.isIfaceMatch;
+import static android.net.cts.util.CtsTetheringUtils.isWifiTetheringSupported;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.UiAutomation;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.TetheringManager;
+import android.net.TetheringManager.OnTetheringEntitlementResultListener;
+import android.net.TetheringManager.TetheringInterfaceRegexps;
+import android.net.TetheringManager.TetheringRequest;
+import android.net.cts.util.CtsNetUtils;
+import android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+import android.net.cts.util.CtsTetheringUtils;
+import android.net.cts.util.CtsTetheringUtils.StartTetheringCallback;
+import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
+import android.net.wifi.WifiManager;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.os.ResultReceiver;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+public class TetheringManagerTest {
+
+ private Context mContext;
+
+ private ConnectivityManager mCm;
+ private TetheringManager mTM;
+ private WifiManager mWm;
+ private PackageManager mPm;
+
+ private TetherChangeReceiver mTetherChangeReceiver;
+ private CtsNetUtils mCtsNetUtils;
+ private CtsTetheringUtils mCtsTetheringUtils;
+
+ private static final int DEFAULT_TIMEOUT_MS = 60_000;
+
+ private void adoptShellPermissionIdentity() {
+ final UiAutomation uiAutomation =
+ InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ uiAutomation.adoptShellPermissionIdentity();
+ }
+
+ private void dropShellPermissionIdentity() {
+ final UiAutomation uiAutomation =
+ InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ uiAutomation.dropShellPermissionIdentity();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ adoptShellPermissionIdentity();
+ mContext = InstrumentationRegistry.getContext();
+ mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mTM = (TetheringManager) mContext.getSystemService(Context.TETHERING_SERVICE);
+ mWm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ mPm = mContext.getPackageManager();
+ mCtsNetUtils = new CtsNetUtils(mContext);
+ mCtsTetheringUtils = new CtsTetheringUtils(mContext);
+ mTetherChangeReceiver = new TetherChangeReceiver();
+ final IntentFilter filter = new IntentFilter(
+ TetheringManager.ACTION_TETHER_STATE_CHANGED);
+ final Intent intent = mContext.registerReceiver(mTetherChangeReceiver, filter);
+ if (intent != null) mTetherChangeReceiver.onReceive(null, intent);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mTM.stopAllTethering();
+ mContext.unregisterReceiver(mTetherChangeReceiver);
+ dropShellPermissionIdentity();
+ }
+
+ private class TetherChangeReceiver extends BroadcastReceiver {
+ private class TetherState {
+ final ArrayList<String> mAvailable;
+ final ArrayList<String> mActive;
+ final ArrayList<String> mErrored;
+
+ TetherState(Intent intent) {
+ mAvailable = intent.getStringArrayListExtra(
+ TetheringManager.EXTRA_AVAILABLE_TETHER);
+ mActive = intent.getStringArrayListExtra(
+ TetheringManager.EXTRA_ACTIVE_TETHER);
+ mErrored = intent.getStringArrayListExtra(
+ TetheringManager.EXTRA_ERRORED_TETHER);
+ }
+ }
+
+ @Override
+ public void onReceive(Context content, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(TetheringManager.ACTION_TETHER_STATE_CHANGED)) {
+ mResult.add(new TetherState(intent));
+ }
+ }
+
+ public final LinkedBlockingQueue<TetherState> mResult = new LinkedBlockingQueue<>();
+
+ // Expects that tethering reaches the desired state.
+ // - If active is true, expects that tethering is enabled on at least one interface
+ // matching ifaceRegexs.
+ // - If active is false, expects that tethering is disabled on all the interfaces matching
+ // ifaceRegexs.
+ // Fails if any interface matching ifaceRegexs becomes errored.
+ public void expectTethering(final boolean active, final String[] ifaceRegexs) {
+ while (true) {
+ final TetherState state = pollAndAssertNoError(DEFAULT_TIMEOUT_MS, ifaceRegexs);
+ assertNotNull("Did not receive expected state change, active: " + active, state);
+
+ if (isIfaceActive(ifaceRegexs, state) == active) return;
+ }
+ }
+
+ private TetherState pollAndAssertNoError(final int timeout, final String[] ifaceRegexs) {
+ final TetherState state = pollTetherState(timeout);
+ assertNoErroredIfaces(state, ifaceRegexs);
+ return state;
+ }
+
+ private TetherState pollTetherState(final int timeout) {
+ try {
+ return mResult.poll(timeout, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ fail("No result after " + timeout + " ms");
+ return null;
+ }
+ }
+
+ private boolean isIfaceActive(final String[] ifaceRegexs, final TetherState state) {
+ return isIfaceMatch(ifaceRegexs, state.mActive);
+ }
+
+ private void assertNoErroredIfaces(final TetherState state, final String[] ifaceRegexs) {
+ if (state == null || state.mErrored == null) return;
+
+ if (isIfaceMatch(ifaceRegexs, state.mErrored)) {
+ fail("Found failed tethering interfaces: " + Arrays.toString(state.mErrored.toArray()));
+ }
+ }
+ }
+
+ @Test
+ public void testStartTetheringWithStateChangeBroadcast() throws Exception {
+ final TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
+ try {
+ tetherEventCallback.assumeWifiTetheringSupported(mContext);
+ } finally {
+ mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+ }
+
+ final String[] wifiRegexs = mTM.getTetherableWifiRegexs();
+ final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
+ final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setShouldShowEntitlementUi(false).build();
+ mTM.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
+ startTetheringCallback.verifyTetheringStarted();
+
+ mTetherChangeReceiver.expectTethering(true /* active */, wifiRegexs);
+
+ mTM.stopTethering(TETHERING_WIFI);
+ mCtsTetheringUtils.expectSoftApDisabled();
+ mTetherChangeReceiver.expectTethering(false /* active */, wifiRegexs);
+ }
+
+ @Test
+ public void testTetheringRequest() {
+ final TetheringRequest tr = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ assertEquals(TETHERING_WIFI, tr.getTetheringType());
+ assertNull(tr.getLocalIpv4Address());
+ assertNull(tr.getClientStaticIpv4Address());
+ assertFalse(tr.isExemptFromEntitlementCheck());
+ assertTrue(tr.getShouldShowEntitlementUi());
+
+ final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
+ final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
+ final TetheringRequest tr2 = new TetheringRequest.Builder(TETHERING_USB)
+ .setStaticIpv4Addresses(localAddr, clientAddr)
+ .setExemptFromEntitlementCheck(true)
+ .setShouldShowEntitlementUi(false).build();
+
+ assertEquals(localAddr, tr2.getLocalIpv4Address());
+ assertEquals(clientAddr, tr2.getClientStaticIpv4Address());
+ assertEquals(TETHERING_USB, tr2.getTetheringType());
+ assertTrue(tr2.isExemptFromEntitlementCheck());
+ assertFalse(tr2.getShouldShowEntitlementUi());
+ }
+
+ @Test
+ public void testRegisterTetheringEventCallback() throws Exception {
+ final TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
+
+ try {
+ tetherEventCallback.assumeWifiTetheringSupported(mContext);
+
+ mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+
+ final List<String> tetheredIfaces = tetherEventCallback.getTetheredInterfaces();
+ assertEquals(1, tetheredIfaces.size());
+ final String wifiTetheringIface = tetheredIfaces.get(0);
+
+ mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
+
+ try {
+ final int ret = mTM.tether(wifiTetheringIface);
+ // There is no guarantee that the wifi interface will be available after disabling
+ // the hotspot, so don't fail the test if the call to tether() fails.
+ if (ret == TETHER_ERROR_NO_ERROR) {
+ // If calling #tether successful, there is a callback to tell the result of
+ // tethering setup.
+ tetherEventCallback.expectErrorOrTethered(wifiTetheringIface);
+ }
+ } finally {
+ mTM.untether(wifiTetheringIface);
+ }
+ } finally {
+ mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+ }
+ }
+
+ @Test
+ public void testGetTetherableInterfaceRegexps() {
+ final TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
+ tetherEventCallback.assumeTetheringSupported();
+
+ final TetheringInterfaceRegexps tetherableRegexs =
+ tetherEventCallback.getTetheringInterfaceRegexps();
+ final List<String> wifiRegexs = tetherableRegexs.getTetherableWifiRegexs();
+ final List<String> usbRegexs = tetherableRegexs.getTetherableUsbRegexs();
+ final List<String> btRegexs = tetherableRegexs.getTetherableBluetoothRegexs();
+
+ assertEquals(wifiRegexs, Arrays.asList(mTM.getTetherableWifiRegexs()));
+ assertEquals(usbRegexs, Arrays.asList(mTM.getTetherableUsbRegexs()));
+ assertEquals(btRegexs, Arrays.asList(mTM.getTetherableBluetoothRegexs()));
+
+ //Verify that any regex name should only contain in one array.
+ wifiRegexs.forEach(s -> assertFalse(usbRegexs.contains(s)));
+ wifiRegexs.forEach(s -> assertFalse(btRegexs.contains(s)));
+ usbRegexs.forEach(s -> assertFalse(btRegexs.contains(s)));
+
+ mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+ }
+
+ @Test
+ public void testStopAllTethering() throws Exception {
+ final TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
+ try {
+ tetherEventCallback.assumeWifiTetheringSupported(mContext);
+
+ // TODO: start ethernet tethering here when TetheringManagerTest is moved to
+ // TetheringIntegrationTest.
+
+ mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+
+ mTM.stopAllTethering();
+ tetherEventCallback.expectTetheredInterfacesChanged(null);
+ } finally {
+ mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+ }
+ }
+
+ @Test
+ public void testEnableTetheringPermission() throws Exception {
+ dropShellPermissionIdentity();
+ final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
+ mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(),
+ c -> c.run() /* executor */, startTetheringCallback);
+ startTetheringCallback.expectTetheringFailed(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ }
+
+ private class EntitlementResultListener implements OnTetheringEntitlementResultListener {
+ private final CompletableFuture<Integer> future = new CompletableFuture<>();
+
+ @Override
+ public void onTetheringEntitlementResult(int result) {
+ future.complete(result);
+ }
+
+ public int get(long timeout, TimeUnit unit) throws Exception {
+ return future.get(timeout, unit);
+ }
+
+ }
+
+ private void assertEntitlementResult(final Consumer<EntitlementResultListener> functor,
+ final int expect) throws Exception {
+ final EntitlementResultListener listener = new EntitlementResultListener();
+ functor.accept(listener);
+
+ assertEquals(expect, listener.get(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testRequestLatestEntitlementResult() throws Exception {
+ assumeTrue(mTM.isTetheringSupported());
+ // Verify that requestLatestTetheringEntitlementResult() can get entitlement
+ // result(TETHER_ERROR_ENTITLEMENT_UNKNOWN due to invalid downstream type) via listener.
+ assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult(
+ TETHERING_WIFI_P2P, false, c -> c.run(), listener),
+ TETHER_ERROR_ENTITLEMENT_UNKNOWN);
+
+ // Verify that requestLatestTetheringEntitlementResult() can get entitlement
+ // result(TETHER_ERROR_ENTITLEMENT_UNKNOWN due to invalid downstream type) via receiver.
+ assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult(
+ TETHERING_WIFI_P2P,
+ new ResultReceiver(null /* handler */) {
+ @Override
+ public void onReceiveResult(int resultCode, Bundle resultData) {
+ listener.onTetheringEntitlementResult(resultCode);
+ }
+ }, false),
+ TETHER_ERROR_ENTITLEMENT_UNKNOWN);
+
+ // Do not request TETHERING_WIFI entitlement result if TETHERING_WIFI is not available.
+ assumeTrue(mTM.getTetherableWifiRegexs().length > 0);
+
+ // Verify that null listener will cause IllegalArgumentException.
+ try {
+ mTM.requestLatestTetheringEntitlementResult(
+ TETHERING_WIFI, false, c -> c.run(), null);
+ } catch (IllegalArgumentException expect) { }
+
+ // Override carrier config to ignore entitlement check.
+ final PersistableBundle bundle = new PersistableBundle();
+ bundle.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, false);
+ overrideCarrierConfig(bundle);
+
+ // Verify that requestLatestTetheringEntitlementResult() can get entitlement
+ // result TETHER_ERROR_NO_ERROR due to provisioning bypassed.
+ assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult(
+ TETHERING_WIFI, false, c -> c.run(), listener), TETHER_ERROR_NO_ERROR);
+
+ // Reset carrier config.
+ overrideCarrierConfig(null);
+ }
+
+ private void overrideCarrierConfig(PersistableBundle bundle) {
+ final CarrierConfigManager configManager = (CarrierConfigManager) mContext
+ .getSystemService(Context.CARRIER_CONFIG_SERVICE);
+ final int subId = SubscriptionManager.getDefaultSubscriptionId();
+ configManager.overrideConfig(subId, bundle);
+ }
+
+ @Test
+ public void testTetheringUpstream() throws Exception {
+ assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
+ final TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
+
+ boolean previousWifiEnabledState = false;
+
+ try {
+ tetherEventCallback.assumeWifiTetheringSupported(mContext);
+
+ previousWifiEnabledState = mWm.isWifiEnabled();
+ if (previousWifiEnabledState) {
+ mCtsNetUtils.ensureWifiDisconnected(null);
+ }
+
+ final TestNetworkCallback networkCallback = new TestNetworkCallback();
+ Network activeNetwork = null;
+ try {
+ mCm.registerDefaultNetworkCallback(networkCallback);
+ activeNetwork = networkCallback.waitForAvailable();
+ } finally {
+ mCm.unregisterNetworkCallback(networkCallback);
+ }
+
+ assertNotNull("No active network. Please ensure the device has working mobile data.",
+ activeNetwork);
+ final NetworkCapabilities activeNetCap = mCm.getNetworkCapabilities(activeNetwork);
+
+ // If active nework is ETHERNET, tethering may not use cell network as upstream.
+ assumeFalse(activeNetCap.hasTransport(TRANSPORT_ETHERNET));
+
+ assertTrue(activeNetCap.hasTransport(TRANSPORT_CELLULAR));
+
+ mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+
+ final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(
+ Context.TELEPHONY_SERVICE);
+ final boolean dunRequired = telephonyManager.isTetheringApnRequired();
+ final int expectedCap = dunRequired ? NET_CAPABILITY_DUN : NET_CAPABILITY_INTERNET;
+ final Network network = tetherEventCallback.getCurrentValidUpstream();
+ final NetworkCapabilities netCap = mCm.getNetworkCapabilities(network);
+ assertTrue(netCap.hasTransport(TRANSPORT_CELLULAR));
+ assertTrue(netCap.hasCapability(expectedCap));
+
+ mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
+ } finally {
+ mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+ if (previousWifiEnabledState) {
+ mCtsNetUtils.connectToWifi();
+ }
+ }
+ }
+}