Merge history of CTS

BUG: 167962976
Test: TH
Merged-In: I30d52c4df571c894b7797300e8f56ddd4b2cc2dd
Change-Id: Id343f5a31604abfe70140343bffab20503cd2705
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);
+        }
+    }
+}