Merge "Add test for internet availability on portals"
diff --git a/tests/cts/hostside/AndroidTest.xml b/tests/cts/hostside/AndroidTest.xml
index dbff179..6a7f1b2 100644
--- a/tests/cts/hostside/AndroidTest.xml
+++ b/tests/cts/hostside/AndroidTest.xml
@@ -21,6 +21,8 @@
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
 
     <target_preparer class="com.android.cts.net.NetPolicyTestsPreparer" />
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.LocationCheck" />
+    <target_preparer class="com.android.cts.net.NetworkPolicyTestsPreparer" />
 
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="teardown-command" value="cmd power set-mode 0" />
@@ -31,4 +33,9 @@
         <option name="jar" value="CtsHostsideNetworkTests.jar" />
         <option name="runtime-hint" value="3m56s" />
     </test>
+
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="directory-keys" value="/sdcard/CtsHostsideNetworkTests" />
+        <option name="collect-on-run-ended-only" value="true" />
+    </metrics_collector>
 </configuration>
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 2362389..7a11456 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -20,6 +20,8 @@
     //sdk_version: "current",
     platform_apis: true,
     static_libs: [
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
         "ub-uiautomator",
diff --git a/tests/cts/hostside/app/AndroidManifest.xml b/tests/cts/hostside/app/AndroidManifest.xml
index f54b5c1..3b00713 100644
--- a/tests/cts/hostside/app/AndroidManifest.xml
+++ b/tests/cts/hostside/app/AndroidManifest.xml
@@ -26,8 +26,9 @@
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
-    <application>
+    <application android:requestLegacyExternalStorage="true" >
         <uses-library android:name="android.test.runner" />
         <activity android:name=".MyActivity" />
         <service android:name=".MyVpnService"
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
index 55bd406..d06ab13 100644
--- 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
@@ -16,20 +16,27 @@
 
 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 android.util.Log;
+
+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 {
 
-    @Override
-    protected final void setUp() throws Exception {
+    @Before
+    public final void setUp() throws Exception {
         super.setUp();
 
-        if (!isSupported()) return;
-
         // Set initial state.
         removePowerSaveModeWhitelist(TEST_APP2_PKG);
         removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
@@ -39,41 +46,16 @@
         registerBroadcastReceiver();
     }
 
-    @Override
-    protected final void tearDown() throws Exception {
+    @After
+    public final void tearDown() throws Exception {
         super.tearDown();
 
-        if (!isSupported()) return;
-
-        try {
-            tearDownMeteredNetwork();
-        } finally {
-            turnBatteryOff();
-            setAppIdle(false);
-        }
+        turnBatteryOff();
+        setAppIdle(false);
     }
 
-    @Override
-    protected boolean isSupported() throws Exception {
-        boolean supported = isDozeModeEnabled();
-        if (!supported) {
-            Log.i(TAG, "Skipping " + getClass() + "." + getName()
-                    + "() because device does not support Doze Mode");
-        }
-        return supported;
-    }
-
-    /**
-     * Resets the (non) metered network state.
-     *
-     * <p>By default is empty - it's up to subclasses to override.
-     */
-    protected void tearDownMeteredNetwork() throws Exception {
-    }
-
+    @Test
     public void testBackgroundNetworkAccess_enabled() throws Exception {
-        if (!isSupported()) return;
-
         setAppIdle(true);
         assertBackgroundNetworkAccess(false);
 
@@ -98,9 +80,8 @@
         assertBackgroundNetworkAccess(false);
     }
 
+    @Test
     public void testBackgroundNetworkAccess_whitelisted() throws Exception {
-        if (!isSupported()) return;
-
         setAppIdle(true);
         assertBackgroundNetworkAccess(false);
 
@@ -127,9 +108,8 @@
         assertBackgroundNetworkAccess(false);
     }
 
+    @Test
     public void testBackgroundNetworkAccess_tempWhitelisted() throws Exception {
-        if (!isSupported()) return;
-
         setAppIdle(true);
         assertBackgroundNetworkAccess(false);
 
@@ -140,23 +120,17 @@
         assertBackgroundNetworkAccess(false);
     }
 
+    @Test
     public void testBackgroundNetworkAccess_disabled() throws Exception {
-        if (!isSupported()) return;
-
         assertBackgroundNetworkAccess(true);
 
         assertsForegroundAlwaysHasNetworkAccess();
         assertBackgroundNetworkAccess(true);
     }
 
+    @RequiredProperties({BATTERY_SAVER_MODE})
+    @Test
     public void testAppIdleNetworkAccess_whenCharging() throws Exception {
-        if (!isBatterySaverSupported()) {
-            Log.i(TAG, "Skipping " + getClass() + "." + getName()
-                    + "() because device does not support Battery saver mode");
-            return;
-        }
-        if (!isSupported()) return;
-
         // Check that app is paroled when charging
         setAppIdle(true);
         assertBackgroundNetworkAccess(false);
@@ -180,9 +154,8 @@
         assertBackgroundNetworkAccess(true);
     }
 
+    @Test
     public void testAppIdleNetworkAccess_idleWhitelisted() throws Exception {
-        if (!isSupported()) return;
-
         setAppIdle(true);
         assertAppIdle(true);
         assertBackgroundNetworkAccess(false);
@@ -199,9 +172,8 @@
         removeAppIdleWhitelist(mUid + 1);
     }
 
+    @Test
     public void testAppIdle_toast() throws Exception {
-        if (!isSupported()) return;
-
         setAppIdle(true);
         assertAppIdle(true);
         assertEquals("Shown", showToast());
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
index 931376b..04d054d 100644
--- 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
@@ -16,20 +16,22 @@
 
 package com.android.cts.net.hostside;
 
-import android.text.TextUtils;
-import android.util.Log;
+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 {
 
-    @Override
-    protected final void setUp() throws Exception {
+    @Before
+    public final void setUp() throws Exception {
         super.setUp();
 
-        if (!isSupported()) return;
-
         // Set initial state.
         removePowerSaveModeWhitelist(TEST_APP2_PKG);
         removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
@@ -38,55 +40,15 @@
         registerBroadcastReceiver();
     }
 
-    @Override
-    protected final void tearDown() throws Exception {
+    @After
+    public final void tearDown() throws Exception {
         super.tearDown();
 
-        if (!isSupported()) return;
-
-        try {
-            tearDownMeteredNetwork();
-        } finally {
-            setBatterySaverMode(false);
-        }
+        setBatterySaverMode(false);
     }
 
-    @Override
-    protected boolean isSupported() throws Exception {
-        String unSupported = "";
-        if (!isDozeModeEnabled()) {
-            unSupported += "Doze mode,";
-        }
-        if (!isBatterySaverSupported()) {
-            unSupported += "Battery saver mode,";
-        }
-        if (!TextUtils.isEmpty(unSupported)) {
-            Log.i(TAG, "Skipping " + getClass() + "." + getName()
-                    + "() because device does not support " + unSupported);
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Sets the initial (non) metered network state.
-     *
-     * <p>By default is empty - it's up to subclasses to override.
-     */
-    protected void setUpMeteredNetwork() throws Exception {
-    }
-
-    /**
-     * Resets the (non) metered network state.
-     *
-     * <p>By default is empty - it's up to subclasses to override.
-     */
-    protected void tearDownMeteredNetwork() throws Exception {
-    }
-
+    @Test
     public void testBackgroundNetworkAccess_enabled() throws Exception {
-        if (!isSupported()) return;
-
         setBatterySaverMode(true);
         assertBackgroundNetworkAccess(false);
 
@@ -118,9 +80,8 @@
         assertBackgroundNetworkAccess(false);
     }
 
+    @Test
     public void testBackgroundNetworkAccess_whitelisted() throws Exception {
-        if (!isSupported()) return;
-
         setBatterySaverMode(true);
         assertBackgroundNetworkAccess(false);
 
@@ -140,9 +101,8 @@
         assertBackgroundNetworkAccess(false);
     }
 
+    @Test
     public void testBackgroundNetworkAccess_disabled() throws Exception {
-        if (!isSupported()) return;
-
         assertBackgroundNetworkAccess(true);
 
         assertsForegroundAlwaysHasNetworkAccess();
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
index f20f1d1..6f32c56 100644
--- 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
@@ -16,20 +16,25 @@
 
 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 android.util.Log;
+
+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 {
 
-    @Override
-    protected final void setUp() throws Exception {
+    @Before
+    public final void setUp() throws Exception {
         super.setUp();
 
-        if (!isSupported()) return;
-
         // Set initial state.
         removePowerSaveModeWhitelist(TEST_APP2_PKG);
         removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
@@ -38,48 +43,15 @@
         registerBroadcastReceiver();
     }
 
-    @Override
-    protected final void tearDown() throws Exception {
+    @After
+    public final void tearDown() throws Exception {
         super.tearDown();
 
-        if (!isSupported()) return;
-
-        try {
-            tearDownMeteredNetwork();
-        } finally {
-            setDozeMode(false);
-        }
+        setDozeMode(false);
     }
 
-    @Override
-    protected boolean isSupported() throws Exception {
-        boolean supported = isDozeModeEnabled();
-        if (!supported) {
-            Log.i(TAG, "Skipping " + getClass() + "." + getName()
-                    + "() because device does not support Doze Mode");
-        }
-        return supported;
-    }
-
-    /**
-     * Sets the initial (non) metered network state.
-     *
-     * <p>By default is empty - it's up to subclasses to override.
-     */
-    protected void setUpMeteredNetwork() throws Exception {
-    }
-
-    /**
-     * Resets the (non) metered network state.
-     *
-     * <p>By default is empty - it's up to subclasses to override.
-     */
-    protected void tearDownMeteredNetwork() throws Exception {
-    }
-
+    @Test
     public void testBackgroundNetworkAccess_enabled() throws Exception {
-        if (!isSupported()) return;
-
         setDozeMode(true);
         assertBackgroundNetworkAccess(false);
 
@@ -96,9 +68,8 @@
         assertBackgroundNetworkAccess(false);
     }
 
+    @Test
     public void testBackgroundNetworkAccess_whitelisted() throws Exception {
-        if (!isSupported()) return;
-
         setDozeMode(true);
         assertBackgroundNetworkAccess(false);
 
@@ -118,19 +89,18 @@
         assertBackgroundNetworkAccess(false);
     }
 
+    @Test
     public void testBackgroundNetworkAccess_disabled() throws Exception {
-        if (!isSupported()) return;
-
         assertBackgroundNetworkAccess(true);
 
         assertsForegroundAlwaysHasNetworkAccess();
         assertBackgroundNetworkAccess(true);
     }
 
+    @RequiredProperties({NOT_LOW_RAM_DEVICE})
+    @Test
     public void testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction()
             throws Exception {
-        if (!isSupported() || isLowRamDevice()) return;
-
         setPendingIntentWhitelistDuration(NETWORK_TIMEOUT_MS);
         try {
             registerNotificationListenerService();
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
index 40d7e34..c8fe624 100644
--- 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
@@ -17,14 +17,22 @@
 package com.android.cts.net.hostside;
 
 import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-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.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.compatibility.common.util.SystemUtil.runShellCommand;
+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.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.app.ActivityManager;
 import android.app.Instrumentation;
@@ -34,9 +42,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
 import android.net.NetworkInfo.DetailedState;
 import android.net.NetworkInfo.State;
 import android.net.wifi.WifiManager;
@@ -44,24 +50,27 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.os.SystemProperties;
 import android.provider.Settings;
 import android.service.notification.NotificationListenerService;
-import android.test.InstrumentationTestCase;
-import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.compatibility.common.util.BatteryUtils;
+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;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
 /**
  * Superclass for tests related to background network restrictions.
  */
-abstract class AbstractRestrictBackgroundNetworkTestCase extends InstrumentationTestCase {
-    protected static final String TAG = "RestrictBackgroundNetworkTests";
+@RunWith(AndroidJUnit4.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";
@@ -98,8 +107,6 @@
     static final int NETWORK_TIMEOUT_MS = 15 * SECOND_IN_MS;
     private static int PROCESS_STATE_FOREGROUND_SERVICE;
 
-    private static final int PROCESS_STATE_TOP = 2;
-
     private static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
 
     protected static final int TYPE_COMPONENT_ACTIVTIY = 0;
@@ -123,38 +130,31 @@
     protected Context mContext;
     protected Instrumentation mInstrumentation;
     protected ConnectivityManager mCm;
-    protected WifiManager mWfm;
     protected int mUid;
     private int mMyUid;
-    private String mMeteredWifi;
     private MyServiceClient mServiceClient;
     private String mDeviceIdleConstantsSetting;
-    private boolean mSupported;
-    private boolean mIsLocationOn;
 
-    @Override
+    @Rule
+    public final RuleChain mRuleChain = RuleChain.outerRule(new DumpOnFailureRule())
+            .around(new RequiredPropertiesRule())
+            .around(new MeterednessConfigurationRule());
+
     protected void setUp() throws Exception {
-        super.setUp();
 
         PROCESS_STATE_FOREGROUND_SERVICE = (Integer) ActivityManager.class
                 .getDeclaredField("PROCESS_STATE_FOREGROUND_SERVICE").get(null);
         mInstrumentation = getInstrumentation();
-        mContext = mInstrumentation.getContext();
-        mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
-        mWfm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        mContext = getContext();
+        mCm = getConnectivityManager();
         mUid = getUid(TEST_APP2_PKG);
         mMyUid = getUid(mContext.getPackageName());
         mServiceClient = new MyServiceClient(mContext);
         mServiceClient.bind();
         mDeviceIdleConstantsSetting = "device_idle_constants";
-        mIsLocationOn = isLocationOn();
-        if (!mIsLocationOn) {
-            enableLocation();
-        }
-        mSupported = setUpActiveNetworkMeteringState();
         setAppIdle(false);
 
-        Log.i(TAG, "Apps status on " + getName() + ":\n"
+        Log.i(TAG, "Apps status:\n"
                 + "\ttest app: uid=" + mMyUid + ", state=" + getProcessStateByUid(mMyUid) + "\n"
                 + "\tapp2: uid=" + mUid + ", state=" + getProcessStateByUid(mUid));
 
@@ -165,37 +165,10 @@
         final String currentConstants =
                 executeShellCommand("settings get global app_idle_constants");
         assertEquals(appIdleConstants, currentConstants);
-   }
+    }
 
-    @Override
     protected void tearDown() throws Exception {
-        if (!mIsLocationOn) {
-            disableLocation();
-        }
         mServiceClient.unbind();
-
-        super.tearDown();
-    }
-
-    private void enableLocation() throws Exception {
-        Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.LOCATION_MODE,
-                Settings.Secure.LOCATION_MODE_SENSORS_ONLY);
-        assertEquals(Settings.Secure.LOCATION_MODE_SENSORS_ONLY,
-                Settings.Secure.getInt(mContext.getContentResolver(),
-                        Settings.Secure.LOCATION_MODE));
-    }
-
-    private void disableLocation() throws Exception {
-        Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.LOCATION_MODE,
-                Settings.Secure.LOCATION_MODE_OFF);
-        assertEquals(Settings.Secure.LOCATION_MODE_OFF,
-                Settings.Secure.getInt(mContext.getContentResolver(),
-                        Settings.Secure.LOCATION_MODE));
-    }
-
-    private boolean isLocationOn() throws Exception {
-        return Settings.Secure.getInt(mContext.getContentResolver(),
-                Settings.Secure.LOCATION_MODE) != Settings.Secure.LOCATION_MODE_OFF;
     }
 
     protected int getUid(String packageName) throws Exception {
@@ -259,23 +232,8 @@
     protected void assertRestrictBackgroundStatus(int expectedStatus) throws Exception {
         final String status = mServiceClient.getRestrictBackgroundStatus();
         assertNotNull("didn't get API status from app2", status);
-        final String actualStatus = toString(Integer.parseInt(status));
-        assertEquals("wrong status", toString(expectedStatus), actualStatus);
-    }
-
-    protected void assertMyRestrictBackgroundStatus(int expectedStatus) throws Exception {
-        final int actualStatus = mCm.getRestrictBackgroundStatus();
-        assertEquals("Wrong status", toString(expectedStatus), toString(actualStatus));
-    }
-
-    protected boolean isMyRestrictBackgroundStatus(int expectedStatus) throws Exception {
-        final int actualStatus = mCm.getRestrictBackgroundStatus();
-        if (expectedStatus != actualStatus) {
-            Log.d(TAG, "Expected: " + toString(expectedStatus)
-                    + " but actual: " + toString(actualStatus));
-            return false;
-        }
-        return true;
+        assertEquals(restrictBackgroundValueToString(expectedStatus),
+                restrictBackgroundValueToString(Integer.parseInt(status)));
     }
 
     protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
@@ -298,28 +256,6 @@
     }
 
     /**
-     * Whether this device suport this type of test.
-     *
-     * <p>Should be overridden when necessary (but always calling
-     * {@code super.isSupported()} first), and explicitly used before each test
-     * Example:
-     *
-     * <pre><code>
-     * public void testSomething() {
-     *    if (!isSupported()) return;
-     * </code></pre>
-     *
-     * @return {@code true} by default.
-     */
-    protected boolean isSupported() throws Exception {
-        return mSupported;
-    }
-
-    protected boolean isBatterySaverSupported() {
-        return BatteryUtils.isBatterySaverSupported();
-    }
-
-    /**
      * 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
@@ -388,23 +324,6 @@
     }
 
     /**
-     * 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.
-     */
-    protected boolean isDataSaverSupported() throws Exception {
-        assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED);
-        try {
-            setRestrictBackground(true);
-            return !isMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED);
-        } finally {
-            setRestrictBackground(false);
-        }
-    }
-
-    /**
      * Returns whether an app state should be considered "background" for restriction purposes.
      */
     protected boolean isBackground(int state) {
@@ -443,40 +362,10 @@
             // Exponential back-off.
             timeoutMs = Math.min(timeoutMs*2, NETWORK_TIMEOUT_MS);
         }
-        dumpOnFailure();
         fail("Invalid state for expectAvailable=" + expectAvailable + " after " + maxTries
                 + " attempts.\nLast error: " + error);
     }
 
-    private void dumpOnFailure() throws Exception {
-        dumpAllNetworkRules();
-        Log.d(TAG, "Usagestats dump: " + getUsageStatsDump());
-        executeShellCommand("settings get global app_idle_constants");
-    }
-
-    private void dumpAllNetworkRules() throws Exception {
-        final String networkManagementDump = runShellCommand(mInstrumentation,
-                "dumpsys network_management").trim();
-        final String networkPolicyDump = runShellCommand(mInstrumentation,
-                "dumpsys netpolicy").trim();
-        TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter('\n');
-        splitter.setString(networkManagementDump);
-        String next;
-        Log.d(TAG, ">>> Begin network_management dump");
-        while (splitter.hasNext()) {
-            next = splitter.next();
-            Log.d(TAG, next);
-        }
-        Log.d(TAG, "<<< End network_management dump");
-        splitter.setString(networkPolicyDump);
-        Log.d(TAG, ">>> Begin netpolicy dump");
-        while (splitter.hasNext()) {
-            next = splitter.next();
-            Log.d(TAG, next);
-        }
-        Log.d(TAG, "<<< End netpolicy dump");
-    }
-
     /**
      * Checks whether the network is available as expected.
      *
@@ -528,22 +417,10 @@
         return errors.toString();
     }
 
-    protected boolean isLowRamDevice() {
-        final ActivityManager am = (ActivityManager) mContext.getSystemService(
-            Context.ACTIVITY_SERVICE);
-        return am.isLowRamDevice();
-    }
-
-    protected String executeShellCommand(String command) throws Exception {
-        final String result = runShellCommand(mInstrumentation, command).trim();
-        if (DEBUG) Log.d(TAG, "Command '" + command + "' returned '" + result + "'");
-        return result;
-    }
-
     /**
      * Runs a Shell command which is not expected to generate output.
      */
-    protected void executeSilentShellCommand(String command) throws Exception {
+    protected void executeSilentShellCommand(String command) {
         final String result = executeShellCommand(command);
         assertTrue("Command '" + command + "' failed: " + result, result.trim().isEmpty());
     }
@@ -572,10 +449,6 @@
         });
     }
 
-    protected void assertDelayedShellCommand(String command, ExpectResultChecker checker)
-            throws Exception {
-        assertDelayedShellCommand(command, 5, 1, checker);
-    }
     protected void assertDelayedShellCommand(String command, int maxTries, int napTimeSeconds,
             ExpectResultChecker checker) throws Exception {
         String result = "";
@@ -592,159 +465,6 @@
                 + " attempts. Last result: '" + result + "'");
     }
 
-    /**
-     * Sets the initial metering state for the active network.
-     *
-     * <p>It's called on setup and by default does nothing - it's up to the
-     * subclasses to override.
-     *
-     * @return whether the tests in the subclass are supported on this device.
-     */
-    protected boolean setUpActiveNetworkMeteringState() throws Exception {
-        return true;
-    }
-
-    /**
-     * Makes sure the active network is not metered.
-     *
-     * <p>If the device does not supoprt un-metered networks (for example if it
-     * only has cellular data but not wi-fi), it should return {@code false};
-     * otherwise, it should return {@code true} (or fail if the un-metered
-     * network could not be set).
-     *
-     * @return {@code true} if the network is now unmetered.
-     */
-    protected boolean setUnmeteredNetwork() throws Exception {
-        final NetworkInfo info = mCm.getActiveNetworkInfo();
-        assertNotNull("Could not get active network", info);
-        if (!mCm.isActiveNetworkMetered()) {
-            Log.d(TAG, "Active network is not metered: " + info);
-        } else if (info.getType() == ConnectivityManager.TYPE_WIFI) {
-            Log.i(TAG, "Setting active WI-FI network as not metered: " + info );
-            setWifiMeteredStatus(false);
-        } else {
-            Log.d(TAG, "Active network cannot be set to un-metered: " + info);
-            return false;
-        }
-        assertActiveNetworkMetered(false); // Sanity check.
-        return true;
-    }
-
-    /**
-     * Enables metering on the active network if supported.
-     *
-     * <p>If the device does not support metered networks it should return
-     * {@code false}; otherwise, it should return {@code true} (or fail if the
-     * metered network could not be set).
-     *
-     * @return {@code true} if the network is now metered.
-     */
-    protected boolean setMeteredNetwork() throws Exception {
-        final NetworkInfo info = mCm.getActiveNetworkInfo();
-        final boolean metered = mCm.isActiveNetworkMetered();
-        if (metered) {
-            Log.d(TAG, "Active network already metered: " + info);
-            return true;
-        } else if (info.getType() != ConnectivityManager.TYPE_WIFI) {
-            Log.w(TAG, "Active network does not support metering: " + info);
-            return false;
-        } else {
-            Log.w(TAG, "Active network not metered: " + info);
-        }
-        final String netId = setWifiMeteredStatus(true);
-
-        // Set flag so status is reverted on resetMeteredNetwork();
-        mMeteredWifi = netId;
-        // Sanity check.
-        assertWifiMeteredStatus(netId, true);
-        assertActiveNetworkMetered(true);
-        return true;
-    }
-
-    /**
-     * Resets the device metering state to what it was before the test started.
-     *
-     * <p>This reverts any metering changes made by {@code setMeteredNetwork}.
-     */
-    protected void resetMeteredNetwork() throws Exception {
-        if (mMeteredWifi != null) {
-            Log.i(TAG, "resetMeteredNetwork(): SID '" + mMeteredWifi
-                    + "' was set as metered by test case; resetting it");
-            setWifiMeteredStatus(mMeteredWifi, false);
-            assertActiveNetworkMetered(false); // Sanity check.
-        }
-    }
-
-    private void assertActiveNetworkMetered(boolean expected) throws Exception {
-        final int maxTries = 5;
-        NetworkInfo info = null;
-        for (int i = 1; i <= maxTries; i++) {
-            info = mCm.getActiveNetworkInfo();
-            if (info == null) {
-                Log.v(TAG, "No active network info on attempt #" + i
-                        + "; sleeping 1s before polling again");
-            } else if (mCm.isActiveNetworkMetered() != expected) {
-                Log.v(TAG, "Wrong metered status for active network " + info + "; expected="
-                        + expected + "; sleeping 1s before polling again");
-            } else {
-                break;
-            }
-            Thread.sleep(SECOND_IN_MS);
-        }
-        assertNotNull("No active network after " + maxTries + " attempts", info);
-        assertEquals("Wrong metered status for active network " + info, expected,
-                mCm.isActiveNetworkMetered());
-    }
-
-    private String setWifiMeteredStatus(boolean metered) throws Exception {
-        // We could call setWifiEnabled() here, but it might take sometime to be in a consistent
-        // state (for example, if one of the saved network is not properly authenticated), so it's
-        // better to let the hostside test take care of that.
-        assertTrue("wi-fi is disabled", mWfm.isWifiEnabled());
-        // TODO: if it's not guaranteed the device has wi-fi, we need to change the tests
-        // to make the actual verification of restrictions optional.
-        final String ssid = mWfm.getConnectionInfo().getSSID();
-        return setWifiMeteredStatus(ssid, metered);
-    }
-
-    private String setWifiMeteredStatus(String ssid, boolean metered) throws Exception {
-        assertNotNull("null SSID", ssid);
-        final String netId = ssid.trim().replaceAll("\"", ""); // remove quotes, if any.
-        assertFalse("empty SSID", ssid.isEmpty());
-
-        Log.i(TAG, "Setting wi-fi network " + netId + " metered status to " + metered);
-        final String setCommand = "cmd netpolicy set metered-network " + netId + " " + metered;
-        assertDelayedShellCommand(setCommand, "");
-
-        return netId;
-    }
-
-    private void assertWifiMeteredStatus(String netId, boolean status) throws Exception {
-        final String command = "cmd netpolicy list wifi-networks";
-        final String expectedLine = netId + ";" + status;
-        assertDelayedShellCommand(command, new ExpectResultChecker() {
-
-            @Override
-            public boolean isExpected(String result) {
-                return result.contains(expectedLine);
-            }
-
-            @Override
-            public String getExpected() {
-                return "line containing " + expectedLine;
-            }
-        });
-    }
-
-    protected void setRestrictBackground(boolean enabled) throws Exception {
-        executeShellCommand("cmd netpolicy set restrict-background " + enabled);
-        final String output = executeShellCommand("cmd netpolicy get restrict-background ");
-        final String expectedSuffix = enabled ? "enabled" : "disabled";
-        // TODO: use MoreAsserts?
-        assertTrue("output '" + output + "' should end with '" + expectedSuffix + "'",
-                output.endsWith(expectedSuffix));
-      }
-
     protected void addRestrictBackgroundWhitelist(int uid) throws Exception {
         executeShellCommand("cmd netpolicy add restrict-background-whitelist " + uid);
         assertRestrictBackgroundWhitelist(uid, true);
@@ -924,7 +644,7 @@
 
     protected void setDozeMode(boolean enabled) throws Exception {
         // Sanity check, since tests should check beforehand....
-        assertTrue("Device does not support Doze Mode", isDozeModeEnabled());
+        assertTrue("Device does not support Doze Mode", isDozeModeSupported());
 
         Log.i(TAG, "Setting Doze Mode to " + enabled);
         if (enabled) {
@@ -944,43 +664,16 @@
         assertDelayedShellCommand("dumpsys deviceidle get deep", enabled ? "IDLE" : "ACTIVE");
     }
 
-    protected boolean isDozeModeEnabled() throws Exception {
-        final String result = executeShellCommand("cmd deviceidle enabled deep").trim();
-        return result.equals("1");
-    }
-
     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); // Sanity check
     }
 
-    private String getUsageStatsDump() throws Exception {
-        final String output = runShellCommand(mInstrumentation, "dumpsys usagestats").trim();
-        final StringBuilder sb = new StringBuilder();
-        final TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter('\n');
-        splitter.setString(output);
-        String str;
-        while (splitter.hasNext()) {
-            str = splitter.next();
-            if (str.contains("package=")
-                    && !str.contains(TEST_PKG) && !str.contains(TEST_APP2_PKG)) {
-                continue;
-            }
-            if (str.trim().startsWith("config=") || str.trim().startsWith("time=")) {
-                continue;
-            }
-            sb.append(str).append('\n');
-        }
-        return sb.toString();
-    }
-
     protected void assertAppIdle(boolean enabled) throws Exception {
         try {
             assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG, 15, 2, "Idle=" + enabled);
         } catch (Throwable e) {
-            Log.d(TAG, "UsageStats dump:\n" + getUsageStatsDump());
-            executeShellCommand("settings get global app_idle_constants");
             throw e;
         }
     }
@@ -1061,12 +754,10 @@
                         // App didn't come to foreground when the activity is started, so try again.
                         assertForegroundNetworkAccess();
                     } else {
-                        dumpOnFailure();
                         fail("Network is not available for app2 (" + mUid + "): " + errors[0]);
                     }
                 }
             } else {
-                dumpOnFailure();
                 fail("Timed out waiting for network availability status from app2 (" + mUid + ")");
             }
         } else {
@@ -1150,19 +841,6 @@
         }
     }
 
-    private String toString(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;
-        }
-    }
-
     private ProcessState getProcessStateByUid(int uid) throws Exception {
         return new ProcessState(executeShellCommand("cmd activity get-uid-state " + uid));
     }
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
index 622d993..f1858d6 100644
--- 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
@@ -16,15 +16,8 @@
 
 package com.android.cts.net.hostside;
 
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
 public class AppIdleMeteredTest extends AbstractAppIdleTestCase {
-
-    @Override
-    protected boolean setUpActiveNetworkMeteringState() throws Exception {
-        return setMeteredNetwork();
-    }
-
-    @Override
-    protected void tearDownMeteredNetwork() throws Exception {
-        resetMeteredNetwork();
-    }
 }
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
index bde71f9..e737a6d 100644
--- 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
@@ -16,9 +16,8 @@
 
 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 {
-    @Override
-    protected boolean setUpActiveNetworkMeteringState() throws Exception {
-        return setUnmeteredNetwork();
-    }
 }
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
index 3071cfe..c78ca2e 100644
--- 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
@@ -16,15 +16,8 @@
 
 package com.android.cts.net.hostside;
 
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
 public class BatterySaverModeMeteredTest extends AbstractBatterySaverModeTestCase {
-
-    @Override
-    protected boolean setUpActiveNetworkMeteringState() throws Exception {
-        return setMeteredNetwork();
-    }
-
-    @Override
-    protected void tearDownMeteredNetwork() throws Exception {
-        resetMeteredNetwork();
-    }
 }
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
index 6d3076f..fb52a54 100644
--- 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
@@ -16,10 +16,9 @@
 
 package com.android.cts.net.hostside;
 
-public class BatterySaverModeNonMeteredTest extends AbstractBatterySaverModeTestCase {
 
-    @Override
-    protected boolean setUpActiveNetworkMeteringState() throws Exception {
-        return setUnmeteredNetwork();
-    }
+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
index cfe6a73..aa2c914 100644
--- 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
@@ -20,24 +20,33 @@
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
 
-import android.util.Log;
+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"
     };
 
-    private boolean mIsDataSaverSupported;
-
-    @Override
+    @Before
     public void setUp() throws Exception {
         super.setUp();
 
-        mIsDataSaverSupported = isDataSaverSupported();
-
         // Set initial state.
         setRestrictBackground(false);
         removeRestrictBackgroundWhitelist(mUid);
@@ -47,36 +56,15 @@
         assertRestrictBackgroundChangedReceived(0);
    }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         super.tearDown();
 
-        if (!isSupported()) return;
-
-        try {
-            resetMeteredNetwork();
-        } finally {
-            setRestrictBackground(false);
-        }
+        setRestrictBackground(false);
     }
 
-    @Override
-    protected boolean setUpActiveNetworkMeteringState() throws Exception {
-        return setMeteredNetwork();
-    }
-
-    @Override
-    protected boolean isSupported() throws Exception {
-        if (!mIsDataSaverSupported) {
-            Log.i(TAG, "Skipping " + getClass() + "." + getName()
-                    + "() because device does not support Data Saver Mode");
-        }
-        return mIsDataSaverSupported && super.isSupported();
-    }
-
+    @Test
     public void testGetRestrictBackgroundStatus_disabled() throws Exception {
-        if (!isSupported()) return;
-
         assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
 
         // Sanity check: make sure status is always disabled, never whitelisted
@@ -88,9 +76,8 @@
         assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
     }
 
+    @Test
     public void testGetRestrictBackgroundStatus_whitelisted() throws Exception {
-        if (!isSupported()) return;
-
         setRestrictBackground(true);
         assertRestrictBackgroundChangedReceived(1);
         assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
@@ -107,9 +94,8 @@
         assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
     }
 
+    @Test
     public void testGetRestrictBackgroundStatus_enabled() throws Exception {
-        if (!isSupported()) return;
-
         setRestrictBackground(true);
         assertRestrictBackgroundChangedReceived(1);
         assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
@@ -142,9 +128,8 @@
         assertBackgroundNetworkAccess(false);
     }
 
+    @Test
     public void testGetRestrictBackgroundStatus_blacklisted() throws Exception {
-        if (!isSupported()) return;
-
         addRestrictBackgroundBlacklist(mUid);
         assertRestrictBackgroundChangedReceived(1);
         assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
@@ -180,9 +165,8 @@
         assertsForegroundAlwaysHasNetworkAccess();
     }
 
+    @Test
     public void testGetRestrictBackgroundStatus_requiredWhitelistedPackages() throws Exception {
-        if (!isSupported()) return;
-
         final StringBuilder error = new StringBuilder();
         for (String packageName : REQUIRED_WHITELISTED_PACKAGES) {
             int uid = -1;
@@ -202,10 +186,10 @@
         }
     }
 
+    @RequiredProperties({NO_DATA_SAVER_MODE})
     @CddTest(requirement="7.4.7/C-2-2")
+    @Test
     public void testBroadcastNotSentOnUnsupportedDevices() throws Exception {
-        if (isSupported()) return;
-
         setRestrictBackground(true);
         assertRestrictBackgroundChangedReceived(0);
 
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
index e4189af..4306c99 100644
--- 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
@@ -16,15 +16,8 @@
 
 package com.android.cts.net.hostside;
 
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
 public class DozeModeMeteredTest extends AbstractDozeModeTestCase {
-
-    @Override
-    protected boolean setUpActiveNetworkMeteringState() throws Exception {
-        return setMeteredNetwork();
-    }
-
-    @Override
-    protected void tearDownMeteredNetwork() throws Exception {
-        resetMeteredNetwork();
-    }
 }
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
index edbbb9e..1e89f15 100644
--- 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
@@ -16,10 +16,8 @@
 
 package com.android.cts.net.hostside;
 
-public class DozeModeNonMeteredTest extends AbstractDozeModeTestCase {
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
 
-    @Override
-    protected boolean setUpActiveNetworkMeteringState() throws Exception {
-        return setUnmeteredNetwork();
-    }
+@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..cedd62a
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
@@ -0,0 +1,91 @@
+/*
+ * 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_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,
+                    "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
index b1a2186..c9edda6 100644
--- 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
@@ -15,9 +15,21 @@
  */
 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.
@@ -29,12 +41,10 @@
 public class MixedModesTest extends AbstractRestrictBackgroundNetworkTestCase {
     private static final String TAG = "MixedModesTest";
 
-    @Override
+    @Before
     public void setUp() throws Exception {
         super.setUp();
 
-        if (!isSupported()) return;
-
         // Set initial state.
         removeRestrictBackgroundWhitelist(mUid);
         removeRestrictBackgroundBlacklist(mUid);
@@ -44,12 +54,10 @@
         registerBroadcastReceiver();
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         super.tearDown();
 
-        if (!isSupported()) return;
-
         try {
             setRestrictBackground(false);
         } finally {
@@ -57,34 +65,15 @@
         }
     }
 
-    @Override
-    public boolean isSupported() throws Exception {
-        if (!isDozeModeEnabled()) {
-            Log.i(TAG, "Skipping " + getClass() + "." + getName()
-                    + "() because device does not support Doze Mode");
-            return false;
-        }
-        return true;
-    }
-
     /**
      * 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 {
-        if (!isBatterySaverSupported()) {
-            Log.i(TAG, "Skipping " + getClass() + "." + getName()
-                    + "() because device does not support Battery saver mode");
-            return;
-        }
-        if (!isSupported()) return;
-
-        Log.i(TAG, "testDataAndBatterySaverModes_meteredNetwork() tests");
-        if (!setMeteredNetwork()) {
-            Log.w(TAG, "testDataAndBatterySaverModes_meteredNetwork() skipped because "
-                    + "device cannot use a metered network");
-            return;
-        }
-
+        final MeterednessConfigurationRule meterednessConfiguration
+                = new MeterednessConfigurationRule();
+        meterednessConfiguration.configureNetworkMeteredness(true);
         try {
             setRestrictBackground(true);
             setBatterySaverMode(true);
@@ -137,7 +126,7 @@
             removeRestrictBackgroundBlacklist(mUid);
             removePowerSaveModeWhitelist(TEST_APP2_PKG);
         } finally {
-            resetMeteredNetwork();
+            meterednessConfiguration.resetNetworkMeteredness();
         }
     }
 
@@ -145,86 +134,75 @@
      * 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 {
-        if (!isBatterySaverSupported()) {
-            Log.i(TAG, "Skipping " + getClass() + "." + getName()
-                    + "() because device does not support Battery saver mode");
-            return;
+        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();
         }
-        if (!isSupported()) return;
-
-        if (!setUnmeteredNetwork()) {
-            Log.w(TAG, "testDataAndBatterySaverModes_nonMeteredNetwork() skipped because network"
-                    + " is metered");
-            return;
-        }
-        Log.i(TAG, "testDataAndBatterySaverModes_nonMeteredNetwork() tests");
-        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);
     }
 
     /**
      * 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 {
-        if (!isBatterySaverSupported()) {
-            Log.i(TAG, "Skipping " + getClass() + "." + getName()
-                    + "() because device does not support Battery saver mode");
-            return;
-        }
-        if (!isSupported()) {
-            return;
-        }
-
         setBatterySaverMode(true);
         setDozeMode(true);
 
@@ -250,11 +228,9 @@
      * 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 {
-        if (!isSupported()) {
-            return;
-        }
-
         setDozeMode(true);
         setAppIdle(true);
 
@@ -276,11 +252,9 @@
         }
     }
 
+    @RequiredProperties({APP_STANDBY_MODE, DOZE_MODE})
+    @Test
     public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
-        if (!isSupported()) {
-            return;
-        }
-
         setDozeMode(true);
         setAppIdle(true);
 
@@ -299,16 +273,9 @@
         }
     }
 
+    @RequiredProperties({APP_STANDBY_MODE, BATTERY_SAVER_MODE})
+    @Test
     public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
-        if (!isBatterySaverSupported()) {
-            Log.i(TAG, "Skipping " + getClass() + "." + getName()
-                    + "() because device does not support Battery saver mode");
-            return;
-        }
-        if (!isSupported()) {
-            return;
-        }
-
         setBatterySaverMode(true);
         setAppIdle(true);
 
@@ -330,11 +297,9 @@
     /**
      * 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 {
-        if (!isSupported()) {
-            return;
-        }
-
         setDozeMode(true);
         setAppIdle(true);
 
@@ -353,11 +318,9 @@
         }
     }
 
+    @RequiredProperties({APP_STANDBY_MODE, DOZE_MODE})
+    @Test
     public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
-        if (!isSupported()) {
-            return;
-        }
-
         setDozeMode(true);
         setAppIdle(true);
 
@@ -380,16 +343,9 @@
         }
     }
 
+    @RequiredProperties({APP_STANDBY_MODE, BATTERY_SAVER_MODE})
+    @Test
     public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
-        if (!isBatterySaverSupported()) {
-            Log.i(TAG, "Skipping " + getClass() + "." + getName()
-                    + "() because device does not support Battery saver mode");
-            return;
-        }
-        if (!isSupported()) {
-            return;
-        }
-
         setBatterySaverMode(true);
         setAppIdle(true);
 
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
index 24dde9d..ed397b9 100644
--- 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
@@ -16,15 +16,26 @@
 
 package com.android.cts.net.hostside;
 
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+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.assertNull;
+import static org.junit.Assert.fail;
+
 import android.net.Network;
 
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
 import java.util.Objects;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 
 public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCase {
 
-    private boolean mIsDataSaverSupported;
     private Network mNetwork;
     private final TestNetworkCallback mTestNetworkCallback = new TestNetworkCallback();
 
@@ -132,108 +143,122 @@
         }
     }
 
-    @Override
+    @Before
     public void setUp() throws Exception {
         super.setUp();
 
-        mIsDataSaverSupported = isDataSaverSupported();
-
         mNetwork = mCm.getActiveNetwork();
 
-        // Set initial state.
-        setBatterySaverMode(false);
         registerBroadcastReceiver();
 
-        if (!mIsDataSaverSupported) return;
-        setRestrictBackground(false);
         removeRestrictBackgroundWhitelist(mUid);
         removeRestrictBackgroundBlacklist(mUid);
         assertRestrictBackgroundChangedReceived(0);
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         super.tearDown();
 
-        if (!mIsDataSaverSupported) return;
+        setRestrictBackground(false);
+        setBatterySaverMode(false);
+    }
 
+    @RequiredProperties({DATA_SAVER_MODE})
+    @Test
+    public void testOnBlockedStatusChanged_dataSaver() throws Exception {
+        // Initial state
+        setBatterySaverMode(false);
+        setRestrictBackground(false);
+
+        final MeterednessConfigurationRule meterednessConfiguration
+                = new MeterednessConfigurationRule();
+        meterednessConfiguration.configureNetworkMeteredness(true);
         try {
-            resetMeteredNetwork();
+            // Register callback
+            registerNetworkCallback((INetworkCallback.Stub) mTestNetworkCallback);
+            mTestNetworkCallback.expectAvailableCallback(mNetwork);
+            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+
+            // Enable restrict background
+            setRestrictBackground(true);
+            assertBackgroundNetworkAccess(false);
+            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, true);
+
+            // Add to whitelist
+            addRestrictBackgroundWhitelist(mUid);
+            assertBackgroundNetworkAccess(true);
+            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+
+            // Remove from whitelist
+            removeRestrictBackgroundWhitelist(mUid);
+            assertBackgroundNetworkAccess(false);
+            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, true);
         } finally {
+            meterednessConfiguration.resetNetworkMeteredness();
+        }
+
+        // Set to non-metered network
+        meterednessConfiguration.configureNetworkMeteredness(false);
+        try {
+            assertBackgroundNetworkAccess(true);
+            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+
+            // Disable restrict background, should not trigger callback
             setRestrictBackground(false);
+            assertBackgroundNetworkAccess(true);
+            mTestNetworkCallback.assertNoCallback();
+        } finally {
+            meterednessConfiguration.resetNetworkMeteredness();
         }
     }
 
-    public void testOnBlockedStatusChanged_data_saver() throws Exception {
-        if (!mIsDataSaverSupported) return;
-
-        // Prepare metered wifi
-        if (!setMeteredNetwork()) return;
-
-        // Register callback
-        registerNetworkCallback((INetworkCallback.Stub) mTestNetworkCallback);
-        mTestNetworkCallback.expectAvailableCallback(mNetwork);
-        mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
-
-        // Enable restrict background
-        setRestrictBackground(true);
-        assertBackgroundNetworkAccess(false);
-        mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, true);
-
-        // Add to whitelist
-        addRestrictBackgroundWhitelist(mUid);
-        assertBackgroundNetworkAccess(true);
-        mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
-
-        // Remove from whitelist
-        removeRestrictBackgroundWhitelist(mUid);
-        assertBackgroundNetworkAccess(false);
-        mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, true);
-
-        // Set to non-metered network
-        setUnmeteredNetwork();
-        assertBackgroundNetworkAccess(true);
-        mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
-
-        // Disable restrict background, should not trigger callback
+    @RequiredProperties({BATTERY_SAVER_MODE})
+    @Test
+    public void testOnBlockedStatusChanged_powerSaver() throws Exception {
+        // Set initial state.
+        setBatterySaverMode(false);
         setRestrictBackground(false);
-        assertBackgroundNetworkAccess(true);
-        mTestNetworkCallback.assertNoCallback();
-    }
 
+        final MeterednessConfigurationRule meterednessConfiguration
+                = new MeterednessConfigurationRule();
+        meterednessConfiguration.configureNetworkMeteredness(true);
+        try {
+            // Register callback
+            registerNetworkCallback((INetworkCallback.Stub) mTestNetworkCallback);
+            mTestNetworkCallback.expectAvailableCallback(mNetwork);
+            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
 
-    public void testOnBlockedStatusChanged_power_saver() throws Exception {
-        // Prepare metered wifi
-        if (!setMeteredNetwork()) return;
+            // Enable Power Saver
+            setBatterySaverMode(true);
+            assertBackgroundNetworkAccess(false);
+            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, true);
 
-        // Register callback
-        registerNetworkCallback((INetworkCallback.Stub) mTestNetworkCallback);
-        mTestNetworkCallback.expectAvailableCallback(mNetwork);
-        mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
-
-        // Enable Power Saver
-        setBatterySaverMode(true);
-        assertBackgroundNetworkAccess(false);
-        mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, true);
-
-        // Disable Power Saver
-        setBatterySaverMode(false);
-        assertBackgroundNetworkAccess(true);
-        mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+            // Disable Power Saver
+            setBatterySaverMode(false);
+            assertBackgroundNetworkAccess(true);
+            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+        } finally {
+            meterednessConfiguration.resetNetworkMeteredness();
+        }
 
         // Set to non-metered network
-        setUnmeteredNetwork();
-        mTestNetworkCallback.assertNoCallback();
+        meterednessConfiguration.configureNetworkMeteredness(false);
+        try {
+            mTestNetworkCallback.assertNoCallback();
 
-        // Enable Power Saver
-        setBatterySaverMode(true);
-        assertBackgroundNetworkAccess(false);
-        mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, true);
+            // Enable Power Saver
+            setBatterySaverMode(true);
+            assertBackgroundNetworkAccess(false);
+            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, true);
 
-        // Disable Power Saver
-        setBatterySaverMode(false);
-        assertBackgroundNetworkAccess(true);
-        mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+            // Disable Power Saver
+            setBatterySaverMode(false);
+            assertBackgroundNetworkAccess(true);
+            mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+        } finally {
+            meterednessConfiguration.resetNetworkMeteredness();
+        }
     }
 
     // TODO: 1. test against VPN lockdown.
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/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/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/src/com/android/cts/net/HostsideNetworkCallbackTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
index 8d6c4ac..1312085 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
@@ -29,14 +29,14 @@
         uninstallPackage(TEST_APP2_PKG, true);
     }
 
-    public void testOnBlockedStatusChanged_data_saver() throws Exception {
+    public void testOnBlockedStatusChanged_dataSaver() throws Exception {
         runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_data_saver");
+                TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_dataSaver");
     }
 
-    public void testOnBlockedStatusChanged_power_saver() throws Exception {
+    public void testOnBlockedStatusChanged_powerSaver() throws Exception {
         runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_power_saver");
+                TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_powerSaver");
     }
 }
 
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index a2443b3..ce20379 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -79,7 +79,8 @@
     protected void installPackage(String apk) throws FileNotFoundException,
             DeviceNotAvailableException {
         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
-        assertNull(getDevice().installPackage(buildHelper.getTestFile(apk), false));
+        assertNull(getDevice().installPackage(buildHelper.getTestFile(apk),
+                false /* reinstall */, true /* grantPermissions */));
     }
 
     protected void uninstallPackage(String packageName, boolean shouldSucceed)
diff --git a/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java b/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java
new file mode 100644
index 0000000..dfce7da
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.net;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil;
+import com.android.tradefed.targetprep.ITargetPreparer;
+
+public class NetworkPolicyTestsPreparer implements ITargetPreparer {
+    private ITestDevice mDevice;
+    private String mOriginalAppStandbyEnabled;
+
+    @Override
+    public void setUp(TestInformation testInformation) throws DeviceNotAvailableException {
+        mDevice = testInformation.getDevice();
+        mOriginalAppStandbyEnabled = getAppStandbyEnabled();
+        setAppStandbyEnabled("1");
+        LogUtil.CLog.d("Original app_standby_enabled: " + mOriginalAppStandbyEnabled);
+    }
+
+    @Override
+    public void tearDown(TestInformation testInformation, Throwable e)
+            throws DeviceNotAvailableException {
+        setAppStandbyEnabled(mOriginalAppStandbyEnabled);
+    }
+
+    private void setAppStandbyEnabled(String appStandbyEnabled) throws DeviceNotAvailableException {
+        if ("null".equals(appStandbyEnabled)) {
+            executeCmd("settings delete global app_standby_enabled");
+        } else {
+            executeCmd("settings put global app_standby_enabled " + appStandbyEnabled);
+        }
+    }
+
+    private String getAppStandbyEnabled() throws DeviceNotAvailableException {
+        return executeCmd("settings get global app_standby_enabled").trim();
+    }
+
+    private String executeCmd(String cmd) throws DeviceNotAvailableException {
+        final String output = mDevice.executeShellCommand(cmd).trim();
+        LogUtil.CLog.d("Output for '%s': %s", cmd, output);
+        return output;
+    }
+}
diff --git a/tests/cts/net/ipsec/Android.bp b/tests/cts/net/ipsec/Android.bp
index 8c073c9..f1f120b 100644
--- a/tests/cts/net/ipsec/Android.bp
+++ b/tests/cts/net/ipsec/Android.bp
@@ -26,6 +26,7 @@
 
     srcs: [
         "src/**/*.java",
+        ":ike-test-utils",
     ],
 
     static_libs: [
diff --git a/tests/cts/net/ipsec/assets/key/client-a-private-key.key b/tests/cts/net/ipsec/assets/key/client-a-private-key.key
new file mode 100644
index 0000000..22736e9
--- /dev/null
+++ b/tests/cts/net/ipsec/assets/key/client-a-private-key.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCv3CvrCGokJSWL
+8ufg6u9LCW4EezztbktqpC0T+1m98+Ujb8/eJ0L2UaxZ9QBSBAqXxEoeZFBeoCXu
+7ezUd5qUPfIhKLAkQTAyU/KgfhHh4i+MJK5ghPbGDE8r2gKUXOkM6M5//ZCpmu0K
+Y/9uQL6D5bkxEaoWegEO+wSXm+hTTgKDtQKHvRibgdcZkcY0cA9JsLrC/nIkP+7i
+pbBT+VTuV6gAnKIV0nq8zvI3A/Z3nAb5Gt0g3qaqs59StDT0QtuXzJkuZEo3XSrS
+jon+8NjSNzqVbJj95B7+uiH+91VEbMtJYFz2MipKvJQDK7Zlxke7LxRj2xJfksJK
+a92/ncxfAgMBAAECggEAQztaMvW5lm35J8LKsWs/5qEJRX9T8LWs8W0oqq36Riub
+G2wgvR6ndAIPcSjAYZqX7iOl7m6NZ0+0kN63HxdGqovwKIskpAekBGmhpYftED1n
+zh0r6UyMB3UnQ22KdOv8UOokIDxxdNX8728BdUYdT9Ggdkj5jLRB+VcwD0IUlNvo
+zzTpURV9HEd87uiLqd4AAHXSI0lIHI5U43z24HI/J6/YbYHT3Rlh6CIa/LuwO6vL
+gFkgqg0/oy6yJtjrHtzNVA67F0UaH62hR4YFgbC0d955SJnDidWOv/0j2DMpfdCc
+9kFAcPwUSyykvUSLnGIKWSG4D+6gzIeAeUx4oO7kMQKBgQDVNRkX8AGTHyLg+NXf
+spUWWcodwVioXl30Q7h6+4bt8OI61UbhQ7wX61wvJ1cySpa2KOYa2UdagQVhGhhL
+ADu363R77uXF/jZgzVfmjjyJ2nfDqRgHWRTlSkuq/jCOQCz7VIPHRZg5WL/9D4ms
+TAqMjpzqeMfFZI+w4/+xpcJIuQKBgQDTKBy+ZuerWrVT9icWKvLU58o5EVj/2yFy
+GJvKm+wRAAX2WzjNnR4HVd4DmMREVz1BPYby0j5gqjvtDsxYYu39+NT7JvMioLLK
+QPj+7k5geYgNqVgCxB1vP89RhY2X1RLrN9sTXOodgFPeXOQWNYITkGp3eQpx4nTJ
++K/al3oB1wKBgAjnc8nVIyuyxDEjE0OJYMKTM2a0uXAmqMPXxC+Wq5bqVXhhidlE
+i+lv0eTCPtkB1nN7F8kNQ/aaps/cWCFhvBy9P5shagUvzbOTP9WIIS0cq53HRRKh
+fMbqqGhWv05hjb9dUzeSR341n6cA7B3++v3Nwu3j52vt/DZF/1q68nc5AoGAS0SU
+ImbKE/GsizZGLoe2sZ/CHN+LKwCwhlwxRGKaHmE0vuE7eUeVSaYZEo0lAPtb8WJ+
+NRYueASWgeTxgFwbW5mUScZTirdfo+rPFwhZVdhcYApKPgosN9i2DOgfVcz1BnWN
+mPRY25U/0BaqkyQVruWeneG+kGPZn5kPDktKiVcCgYEAkzwU9vCGhm7ZVALvx/zR
+wARz2zsL9ImBc0P4DK1ld8g90FEnHrEgeI9JEwz0zFHOCMLwlk7kG0Xev7vfjZ7G
+xSqtQYOH33Qp6rtBOgdt8hSyDFvakvDl6bqhAw52gelO3MTpAB1+ZsfZ5gFx13Jf
+idNFcaIrC52PtZIH7QCzdDY=
+-----END PRIVATE KEY-----
\ No newline at end of file
diff --git a/tests/cts/net/ipsec/assets/pem/client-a-end-cert.pem b/tests/cts/net/ipsec/assets/pem/client-a-end-cert.pem
new file mode 100644
index 0000000..e82da85
--- /dev/null
+++ b/tests/cts/net/ipsec/assets/pem/client-a-end-cert.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDaDCCAlCgAwIBAgIIcorRI3n29E4wDQYJKoZIhvcNAQELBQAwQTELMAkGA1UE
+BhMCVVMxEDAOBgNVBAoTB0FuZHJvaWQxIDAeBgNVBAMTF3R3by5jYS50ZXN0LmFu
+ZHJvaWQubmV0MB4XDTIwMDQxNDA1MDM0OVoXDTIzMDQxNDA1MDM0OVowRTELMAkG
+A1UEBhMCVVMxEDAOBgNVBAoTB0FuZHJvaWQxJDAiBgNVBAMTG2NsaWVudC50ZXN0
+LmlrZS5hbmRyb2lkLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AK/cK+sIaiQlJYvy5+Dq70sJbgR7PO1uS2qkLRP7Wb3z5SNvz94nQvZRrFn1AFIE
+CpfESh5kUF6gJe7t7NR3mpQ98iEosCRBMDJT8qB+EeHiL4wkrmCE9sYMTyvaApRc
+6Qzozn/9kKma7Qpj/25AvoPluTERqhZ6AQ77BJeb6FNOAoO1Aoe9GJuB1xmRxjRw
+D0mwusL+ciQ/7uKlsFP5VO5XqACcohXSerzO8jcD9necBvka3SDepqqzn1K0NPRC
+25fMmS5kSjddKtKOif7w2NI3OpVsmP3kHv66If73VURsy0lgXPYyKkq8lAMrtmXG
+R7svFGPbEl+Swkpr3b+dzF8CAwEAAaNgMF4wHwYDVR0jBBgwFoAUcqSu1uRYT/DL
+bLoDNUz38nGvCKQwJgYDVR0RBB8wHYIbY2xpZW50LnRlc3QuaWtlLmFuZHJvaWQu
+bmV0MBMGA1UdJQQMMAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBCwUAA4IBAQCa53tK
+I9RM9/MutZ5KNG2Gfs2cqaPyv8ZRhs90HDWZhkFVu7prywJAxOd2hxxHPsvgurio
+4bKAxnT4EXevgz5YoCbj2TPIL9TdFYh59zZ97XXMxk+SRdypgF70M6ETqKPs3hDP
+ZRMMoHvvYaqaPvp4StSBX9A44gSyjHxVYJkrjDZ0uffKg5lFL5IPvqfdmSRSpGab
+SyGTP4OLTy0QiNV3pBsJGdl0h5BzuTPR9OTl4xgeqqBQy2bDjmfJBuiYyCSCkPi7
+T3ohDYCymhuSkuktHPNG1aKllUJaw0tuZuNydlgdAveXPYfM36uvK0sfd9qr9pAy
+rmkYV2MAWguFeckh
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/tests/cts/net/ipsec/assets/pem/client-a-intermediate-ca-one.pem b/tests/cts/net/ipsec/assets/pem/client-a-intermediate-ca-one.pem
new file mode 100644
index 0000000..707e575
--- /dev/null
+++ b/tests/cts/net/ipsec/assets/pem/client-a-intermediate-ca-one.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDaDCCAlCgAwIBAgIIIbjMyRn2770wDQYJKoZIhvcNAQELBQAwQjELMAkGA1UE
+BhMCVVMxEDAOBgNVBAoTB0FuZHJvaWQxITAfBgNVBAMTGHJvb3QuY2EudGVzdC5h
+bmRyb2lkLm5ldDAeFw0xOTA5MzAxODQzMThaFw0yNDA5MjgxODQzMThaMEExCzAJ
+BgNVBAYTAlVTMRAwDgYDVQQKEwdBbmRyb2lkMSAwHgYDVQQDExdvbmUuY2EudGVz
+dC5hbmRyb2lkLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKNN
+sRr5Z30rAEw2jrAh/BIekbEy/MvOucAr1w0lxH71p+ybRBx5Bj7G07UGXbL659gm
+meMV6nabY4HjQXNMq22POiJBZj+U+rw34br6waljBttxCmmJac1VvgqNsSspXjRy
+NbiVQdFjyKSX0NOPcEkwANk15mZbOgJBaYYc8jQCY2G/p8eARVBTLJCy8LEwEU6j
+XRv/4eYST79qpBFc7gQQj2FLmh9oppDIvcIVBHwtd1tBoVuehRSud1o8vQRkl/HJ
+Mrwp24nO5YYhmVNSFRtBpmWMSu1KknFUwkOebINUNsKXXHebVa7cP4XIQUL8mRT3
+5X9rFJFSQJE01S3NjNMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B
+Af8EBAMCAQYwHQYDVR0OBBYEFHK3FIm7g8dxEIwK9zMAO8EWhRYxMB8GA1UdIwQY
+MBaAFEmfqEeF14Nj91ekIpR+sVhCEoAaMA0GCSqGSIb3DQEBCwUAA4IBAQAeMlXT
+TnxZo8oz0204gKZ63RzlgDpJ7SqA3qFG+pV+TiqGfSuVkXuIdOskjxJnA9VxUzrr
+LdMTCn5e0FK6wCYjZ2GT/CD7oD3vSMkzGbLGNcNJhhDHUq8BOLPkPzz/rwQFPBSb
+zr6hsiVXphEt/psGoN7Eu9blPeQaIwMfWnaufAwF664S/3dmCRbNMWSam1qzzz8q
+jr0cDOIMa//ZIAcM16cvoBK6pFGnUmuoJYYRtfpY5MmfCWz0sCJxENIX/lxyhd7N
+FdRALA1ZP3E//Tn2vQoeFjbKaAba527RE26HgHJ9zZDo1nn8J8J/YwYRJdBWM/3S
+LYebNiMtcyB5nIkj
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/tests/cts/net/ipsec/assets/pem/client-a-intermediate-ca-two.pem b/tests/cts/net/ipsec/assets/pem/client-a-intermediate-ca-two.pem
new file mode 100644
index 0000000..39808f8
--- /dev/null
+++ b/tests/cts/net/ipsec/assets/pem/client-a-intermediate-ca-two.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDZzCCAk+gAwIBAgIIKWCREnNCs+wwDQYJKoZIhvcNAQELBQAwQTELMAkGA1UE
+BhMCVVMxEDAOBgNVBAoTB0FuZHJvaWQxIDAeBgNVBAMTF29uZS5jYS50ZXN0LmFu
+ZHJvaWQubmV0MB4XDTE5MDkzMDE4NDQwMloXDTI0MDkyODE4NDQwMlowQTELMAkG
+A1UEBhMCVVMxEDAOBgNVBAoTB0FuZHJvaWQxIDAeBgNVBAMTF3R3by5jYS50ZXN0
+LmFuZHJvaWQubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxLUa
+RqkYl2m7lUmMnkooqO0DNNY1aN9r7mJc3ndYn5gjkpb3yLgOYPDNLcQerV6uWk/u
+qKudNHed2dInGonl3oxwwv7++6oUvvtrSWLDZlRg16GsdIE1Y98DSMQWkSxevYy9
+Nh6FGTdlBFQVMpiMa8qHEkrOyKsy85yCW1sgzlpGTIBwbDAqYtwe3rgbwyHwUtfy
+0EU++DBcR4ll/pDqB0OQtW5E3AOq2GH1iaGeFLKSUQ5KAbdI8y4/b8IkSDffvxcc
+kXig7S54aLrNlL/ZjQ+H4Chgjj2A5wMucd81+Fb60Udej73ICL9PpMPnXQ1+BVYd
+MJ/txjLNmrOJG9yEHQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB
+/wQEAwIBBjAdBgNVHQ4EFgQUcqSu1uRYT/DLbLoDNUz38nGvCKQwHwYDVR0jBBgw
+FoAUcrcUibuDx3EQjAr3MwA7wRaFFjEwDQYJKoZIhvcNAQELBQADggEBADY461GT
+Rw0dGnD07xaGJcI0i0pV+WnGSrl1s1PAIdMYihJAqYnh10fXbFXLm2WMWVmv/pxs
+FI/xDJno+pd4mCa/sIhm63ar/Nv+lFQmcpIlvSlKnhhV4SLNBeqbVhPBGTCHfrG4
+aIyCwm1KJsnkWbf03crhSskR/2CXIjX6lcAy7K3fE2u1ELpAdH0kMJR7VXkLFLUm
+gqe9YCluR0weMpe2sCaOGzdVzQSmMMCzGP5cxeFR5U6K40kMOpiW11JNmQ06xI/m
+YVkMNwoiV/ITT0/C/g9FxJmkO0mVSLEqxaLS/hNiQNDlroVM0rbxhzviXLI3R3AO
+50VvlOQYGxWed/I=
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/tests/cts/net/ipsec/assets/pem/server-a-self-signed-ca.pem b/tests/cts/net/ipsec/assets/pem/server-a-self-signed-ca.pem
new file mode 100644
index 0000000..972fd55
--- /dev/null
+++ b/tests/cts/net/ipsec/assets/pem/server-a-self-signed-ca.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDSDCCAjCgAwIBAgIITJQJ6HC1rjwwDQYJKoZIhvcNAQELBQAwQjELMAkGA1UE
+BhMCVVMxEDAOBgNVBAoTB0FuZHJvaWQxITAfBgNVBAMTGHJvb3QuY2EudGVzdC5h
+bmRyb2lkLm5ldDAeFw0xOTA5MzAxNzU1NTJaFw0yOTA5MjcxNzU1NTJaMEIxCzAJ
+BgNVBAYTAlVTMRAwDgYDVQQKEwdBbmRyb2lkMSEwHwYDVQQDExhyb290LmNhLnRl
+c3QuYW5kcm9pZC5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCT
+q3hGF+JvLaB1xW7KGKmaxiQ7BxX2Sn7cbp7ggoVYXsFlBUuPPv3+Vg5PfPCPhsJ8
+/7w4HyKo3uc/vHs5HpQ7rSd9blhAkfmJci2ULLq73FB8Mix4CzPwMx29RrN1X9bU
+z4G0vJMczIBGxbZ0uw7n8bKcXBV7AIeax+J8lseEZ3k8iSuBkUJqGIpPFKTqByFZ
+A1Lvt47xkON5SZh6c/Oe+o6291wXaCOJUSAKv6PAWZkq9HeD2fqKA/ck9dBaz1M3
+YvzQ9V/7so3/dECjAfKia388h1I6XSGNUM+d5hpxMXpAFgG42eUXHpJ10OjDvSwd
+7ZSC91/kRQewUomEKBK1AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P
+AQH/BAQDAgEGMB0GA1UdDgQWBBRJn6hHhdeDY/dXpCKUfrFYQhKAGjANBgkqhkiG
+9w0BAQsFAAOCAQEAig/94aGfHBhZuvbbhwAK4rUNpizmR567u0ZJ+QUEKyAlo9lT
+ZWYHSm7qTAZYvPEjzTQIptnAlxCHePXh3Cfwgo+r82lhG2rcdI03iRyvHWjM8gyk
+BXCJTi0Q08JHHpTP6GnAqpz58qEIFkk8P766zNXdhYrGPOydF+p7MFcb1Zv1gum3
+zmRLt0XUAMfjPUv1Bl8kTKFxH5lkMBLR1E0jnoJoTTfgRPrf9CuFSoh48n7YhoBT
+KV75xZY8b8+SuB0v6BvQmkpKZGoxBjuVsShyG7q1+4JTAtwhiP7BlkDvVkaBEi7t
+WIMFp2r2ZDisHgastNaeYFyzHYz9g1FCCrHQ4w==
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/tests/cts/net/ipsec/src/android/net/eap/cts/EapSessionConfigTest.java b/tests/cts/net/ipsec/src/android/net/eap/cts/EapSessionConfigTest.java
new file mode 100644
index 0000000..c24379d
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/eap/cts/EapSessionConfigTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.eap.cts;
+
+import static android.telephony.TelephonyManager.APPTYPE_USIM;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.eap.EapSessionConfig;
+import android.net.eap.EapSessionConfig.EapAkaConfig;
+import android.net.eap.EapSessionConfig.EapAkaPrimeConfig;
+import android.net.eap.EapSessionConfig.EapMsChapV2Config;
+import android.net.eap.EapSessionConfig.EapSimConfig;
+import android.net.eap.EapSessionConfig.EapUiccConfig;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class EapSessionConfigTest {
+    // These constants are IANA-defined values and are copies of hidden constants in
+    // frameworks/opt/net/ike/src/java/com/android/internal/net/eap/message/EapData.java.
+    private static final int EAP_TYPE_SIM = 18;
+    private static final int EAP_TYPE_AKA = 23;
+    private static final int EAP_TYPE_MSCHAP_V2 = 26;
+    private static final int EAP_TYPE_AKA_PRIME = 50;
+
+    private static final int SUB_ID = 1;
+    private static final byte[] EAP_IDENTITY = "test@android.net".getBytes();
+    private static final String NETWORK_NAME = "android.net";
+    private static final String EAP_MSCHAPV2_USERNAME = "username";
+    private static final String EAP_MSCHAPV2_PASSWORD = "password";
+
+    @Test
+    public void testBuildWithAllEapMethods() {
+        EapSessionConfig result =
+                new EapSessionConfig.Builder()
+                        .setEapIdentity(EAP_IDENTITY)
+                        .setEapSimConfig(SUB_ID, APPTYPE_USIM)
+                        .setEapAkaConfig(SUB_ID, APPTYPE_USIM)
+                        .setEapAkaPrimeConfig(
+                                SUB_ID,
+                                APPTYPE_USIM,
+                                NETWORK_NAME,
+                                true /* allowMismatchedNetworkNames */)
+                        .setEapMsChapV2Config(EAP_MSCHAPV2_USERNAME, EAP_MSCHAPV2_PASSWORD)
+                        .build();
+
+        assertArrayEquals(EAP_IDENTITY, result.getEapIdentity());
+
+        EapSimConfig eapSimConfig = result.getEapSimConfig();
+        assertNotNull(eapSimConfig);
+        assertEquals(EAP_TYPE_SIM, eapSimConfig.getMethodType());
+        verifyEapUiccConfigCommon(eapSimConfig);
+
+        EapAkaConfig eapAkaConfig = result.getEapAkaConfig();
+        assertNotNull(eapAkaConfig);
+        assertEquals(EAP_TYPE_AKA, eapAkaConfig.getMethodType());
+        verifyEapUiccConfigCommon(eapAkaConfig);
+
+        EapAkaPrimeConfig eapAkaPrimeConfig = result.getEapAkaPrimeConfig();
+        assertNotNull(eapAkaPrimeConfig);
+        assertEquals(EAP_TYPE_AKA_PRIME, eapAkaPrimeConfig.getMethodType());
+        assertEquals(NETWORK_NAME, eapAkaPrimeConfig.getNetworkName());
+        assertTrue(NETWORK_NAME, eapAkaPrimeConfig.allowsMismatchedNetworkNames());
+        verifyEapUiccConfigCommon(eapAkaPrimeConfig);
+
+        EapMsChapV2Config eapMsChapV2Config = result.getEapMsChapV2onfig();
+        assertNotNull(eapMsChapV2Config);
+        assertEquals(EAP_TYPE_MSCHAP_V2, eapMsChapV2Config.getMethodType());
+        assertEquals(EAP_MSCHAPV2_USERNAME, eapMsChapV2Config.getUsername());
+        assertEquals(EAP_MSCHAPV2_PASSWORD, eapMsChapV2Config.getPassword());
+    }
+
+    private void verifyEapUiccConfigCommon(EapUiccConfig config) {
+        assertEquals(SUB_ID, config.getSubId());
+        assertEquals(APPTYPE_USIM, config.getAppType());
+    }
+}
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeIdentificationTest.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeIdentificationTest.java
new file mode 100644
index 0000000..0317def
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeIdentificationTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.ipsec.ike.cts;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.ipsec.ike.IkeDerAsn1DnIdentification;
+import android.net.ipsec.ike.IkeFqdnIdentification;
+import android.net.ipsec.ike.IkeIpv4AddrIdentification;
+import android.net.ipsec.ike.IkeIpv6AddrIdentification;
+import android.net.ipsec.ike.IkeKeyIdIdentification;
+import android.net.ipsec.ike.IkeRfc822AddrIdentification;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import javax.security.auth.x500.X500Principal;
+
+@RunWith(AndroidJUnit4.class)
+public final class IkeIdentificationTest extends IkeTestBase {
+    @Test
+    public void testIkeDerAsn1DnIdentification() throws Exception {
+        X500Principal asn1Dn = new X500Principal(LOCAL_ASN1_DN_STRING);
+
+        IkeDerAsn1DnIdentification ikeId = new IkeDerAsn1DnIdentification(asn1Dn);
+        assertEquals(asn1Dn, ikeId.derAsn1Dn);
+    }
+
+    @Test
+    public void testIkeFqdnIdentification() throws Exception {
+        IkeFqdnIdentification ikeId = new IkeFqdnIdentification(LOCAL_HOSTNAME);
+        assertEquals(LOCAL_HOSTNAME, ikeId.fqdn);
+    }
+
+    @Test
+    public void testIkeIpv4AddrIdentification() throws Exception {
+        IkeIpv4AddrIdentification ikeId = new IkeIpv4AddrIdentification(IPV4_ADDRESS_LOCAL);
+        assertEquals(IPV4_ADDRESS_LOCAL, ikeId.ipv4Address);
+    }
+
+    @Test
+    public void testIkeIpv6AddrIdentification() throws Exception {
+        IkeIpv6AddrIdentification ikeId = new IkeIpv6AddrIdentification(IPV6_ADDRESS_LOCAL);
+        assertEquals(IPV6_ADDRESS_LOCAL, ikeId.ipv6Address);
+    }
+
+    @Test
+    public void testIkeKeyIdIdentification() throws Exception {
+        IkeKeyIdIdentification ikeId = new IkeKeyIdIdentification(LOCAL_KEY_ID);
+        assertArrayEquals(LOCAL_KEY_ID, ikeId.keyId);
+    }
+
+    @Test
+    public void testIkeRfc822AddrIdentification() throws Exception {
+        IkeRfc822AddrIdentification ikeId = new IkeRfc822AddrIdentification(LOCAL_RFC822_NAME);
+        assertEquals(LOCAL_RFC822_NAME, ikeId.rfc822Name);
+    }
+}
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionParamsTest.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionParamsTest.java
new file mode 100644
index 0000000..6fc7cb3
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionParamsTest.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.ipsec.ike.cts;
+
+import static android.net.ipsec.ike.IkeSessionParams.IKE_OPTION_ACCEPT_ANY_REMOTE_ID;
+import static android.net.ipsec.ike.IkeSessionParams.IKE_OPTION_EAP_ONLY_AUTH;
+import static android.net.ipsec.ike.IkeSessionParams.IkeAuthConfig;
+import static android.net.ipsec.ike.IkeSessionParams.IkeAuthDigitalSignLocalConfig;
+import static android.net.ipsec.ike.IkeSessionParams.IkeAuthDigitalSignRemoteConfig;
+import static android.net.ipsec.ike.IkeSessionParams.IkeAuthEapConfig;
+import static android.net.ipsec.ike.IkeSessionParams.IkeAuthPskConfig;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.telephony.TelephonyManager.APPTYPE_USIM;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.eap.EapSessionConfig;
+import android.net.ipsec.ike.IkeFqdnIdentification;
+import android.net.ipsec.ike.IkeIdentification;
+import android.net.ipsec.ike.IkeSaProposal;
+import android.net.ipsec.ike.IkeSessionParams;
+import android.net.ipsec.ike.IkeSessionParams.ConfigRequestIpv4PcscfServer;
+import android.net.ipsec.ike.IkeSessionParams.ConfigRequestIpv6PcscfServer;
+import android.net.ipsec.ike.IkeSessionParams.IkeConfigRequest;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.net.ipsec.ike.testutils.CertUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
+    private static final int HARD_LIFETIME_SECONDS = (int) TimeUnit.HOURS.toSeconds(20L);
+    private static final int SOFT_LIFETIME_SECONDS = (int) TimeUnit.HOURS.toSeconds(10L);
+    private static final int DPD_DELAY_SECONDS = (int) TimeUnit.MINUTES.toSeconds(10L);
+    private static final int[] RETRANS_TIMEOUT_MS_LIST = new int[] {500, 500, 500, 500, 500, 500};
+
+    private static final Map<Class<? extends IkeConfigRequest>, Integer> EXPECTED_REQ_COUNT =
+            new HashMap<>();
+    private static final HashSet<InetAddress> EXPECTED_PCSCF_SERVERS = new HashSet<>();
+
+    static {
+        EXPECTED_REQ_COUNT.put(ConfigRequestIpv4PcscfServer.class, 3);
+        EXPECTED_REQ_COUNT.put(ConfigRequestIpv6PcscfServer.class, 3);
+
+        EXPECTED_PCSCF_SERVERS.add(PCSCF_IPV4_ADDRESS_1);
+        EXPECTED_PCSCF_SERVERS.add(PCSCF_IPV4_ADDRESS_2);
+        EXPECTED_PCSCF_SERVERS.add(PCSCF_IPV6_ADDRESS_1);
+        EXPECTED_PCSCF_SERVERS.add(PCSCF_IPV6_ADDRESS_2);
+    }
+
+    // Arbitrary proposal and remote ID. Local ID is chosen to match the client end cert in the
+    // following CL
+    private static final IkeSaProposal SA_PROPOSAL =
+            SaProposalTest.buildIkeSaProposalWithNormalModeCipher();
+    private static final IkeIdentification LOCAL_ID = new IkeFqdnIdentification(LOCAL_HOSTNAME);
+    private static final IkeIdentification REMOTE_ID = new IkeFqdnIdentification(REMOTE_HOSTNAME);
+
+    private static final EapSessionConfig EAP_ALL_METHODS_CONFIG =
+            createEapOnlySafeMethodsBuilder()
+                    .setEapMsChapV2Config(EAP_MSCHAPV2_USERNAME, EAP_MSCHAPV2_PASSWORD)
+                    .build();
+    private static final EapSessionConfig EAP_ONLY_SAFE_METHODS_CONFIG =
+            createEapOnlySafeMethodsBuilder().build();
+
+    private X509Certificate mServerCaCert;
+    private X509Certificate mClientEndCert;
+    private X509Certificate mClientIntermediateCaCertOne;
+    private X509Certificate mClientIntermediateCaCertTwo;
+    private RSAPrivateKey mClientPrivateKey;
+
+    @Before
+    public void setUp() throws Exception {
+        mServerCaCert = CertUtils.createCertFromPemFile("server-a-self-signed-ca.pem");
+        mClientEndCert = CertUtils.createCertFromPemFile("client-a-end-cert.pem");
+        mClientIntermediateCaCertOne =
+                CertUtils.createCertFromPemFile("client-a-intermediate-ca-one.pem");
+        mClientIntermediateCaCertTwo =
+                CertUtils.createCertFromPemFile("client-a-intermediate-ca-two.pem");
+        mClientPrivateKey = CertUtils.createRsaPrivateKeyFromKeyFile("client-a-private-key.key");
+    }
+
+    private static EapSessionConfig.Builder createEapOnlySafeMethodsBuilder() {
+        return new EapSessionConfig.Builder()
+                .setEapIdentity(EAP_IDENTITY)
+                .setEapSimConfig(SUB_ID, APPTYPE_USIM)
+                .setEapAkaConfig(SUB_ID, APPTYPE_USIM)
+                .setEapAkaPrimeConfig(
+                        SUB_ID, APPTYPE_USIM, NETWORK_NAME, true /* allowMismatchedNetworkNames */);
+    }
+
+    /**
+     * Create a Builder that has minimum configurations to build an IkeSessionParams.
+     *
+     * <p>Authentication method is arbitrarily selected. Using other method (e.g. setAuthEap) also
+     * works.
+     */
+    private IkeSessionParams.Builder createIkeParamsBuilderMinimum() {
+        return new IkeSessionParams.Builder(sContext)
+                .setNetwork(sTunNetwork)
+                .setServerHostname(IPV4_ADDRESS_REMOTE.getHostAddress())
+                .addSaProposal(SA_PROPOSAL)
+                .setLocalIdentification(LOCAL_ID)
+                .setRemoteIdentification(REMOTE_ID)
+                .setAuthPsk(IKE_PSK);
+    }
+
+    /**
+     * Verify the minimum configurations to build an IkeSessionParams.
+     *
+     * @see #createIkeParamsBuilderMinimum
+     */
+    private void verifyIkeParamsMinimum(IkeSessionParams sessionParams) {
+        assertEquals(sTunNetwork, sessionParams.getNetwork());
+        assertEquals(IPV4_ADDRESS_REMOTE.getHostAddress(), sessionParams.getServerHostname());
+        assertEquals(Arrays.asList(SA_PROPOSAL), sessionParams.getSaProposals());
+        assertEquals(LOCAL_ID, sessionParams.getLocalIdentification());
+        assertEquals(REMOTE_ID, sessionParams.getRemoteIdentification());
+
+        IkeAuthConfig localConfig = sessionParams.getLocalAuthConfig();
+        assertTrue(localConfig instanceof IkeAuthPskConfig);
+        assertArrayEquals(IKE_PSK, ((IkeAuthPskConfig) localConfig).getPsk());
+        IkeAuthConfig remoteConfig = sessionParams.getRemoteAuthConfig();
+        assertTrue(remoteConfig instanceof IkeAuthPskConfig);
+        assertArrayEquals(IKE_PSK, ((IkeAuthPskConfig) remoteConfig).getPsk());
+    }
+
+    @Test
+    public void testBuildWithMinimumSet() throws Exception {
+        IkeSessionParams sessionParams = createIkeParamsBuilderMinimum().build();
+
+        verifyIkeParamsMinimum(sessionParams);
+
+        // Verify default values that do not need explicit configuration. Do not do assertEquals
+        // to be avoid being a change-detector test
+        assertTrue(sessionParams.getHardLifetimeSeconds() > sessionParams.getSoftLifetimeSeconds());
+        assertTrue(sessionParams.getSoftLifetimeSeconds() > 0);
+        assertTrue(sessionParams.getDpdDelaySeconds() > 0);
+        assertTrue(sessionParams.getRetransmissionTimeoutsMillis().length > 0);
+        for (int timeout : sessionParams.getRetransmissionTimeoutsMillis()) {
+            assertTrue(timeout > 0);
+        }
+        assertTrue(sessionParams.getConfigurationRequests().isEmpty());
+        assertFalse(sessionParams.hasIkeOption(IKE_OPTION_ACCEPT_ANY_REMOTE_ID));
+    }
+
+    @Test
+    public void testSetLifetimes() throws Exception {
+        IkeSessionParams sessionParams =
+                createIkeParamsBuilderMinimum()
+                        .setLifetimeSeconds(HARD_LIFETIME_SECONDS, SOFT_LIFETIME_SECONDS)
+                        .build();
+
+        verifyIkeParamsMinimum(sessionParams);
+        assertEquals(HARD_LIFETIME_SECONDS, sessionParams.getHardLifetimeSeconds());
+        assertEquals(SOFT_LIFETIME_SECONDS, sessionParams.getSoftLifetimeSeconds());
+    }
+
+    @Test
+    public void testSetDpdDelay() throws Exception {
+        IkeSessionParams sessionParams =
+                createIkeParamsBuilderMinimum().setDpdDelaySeconds(DPD_DELAY_SECONDS).build();
+
+        verifyIkeParamsMinimum(sessionParams);
+        assertEquals(DPD_DELAY_SECONDS, sessionParams.getDpdDelaySeconds());
+    }
+
+    @Test
+    public void testSetRetransmissionTimeouts() throws Exception {
+        IkeSessionParams sessionParams =
+                createIkeParamsBuilderMinimum()
+                        .setRetransmissionTimeoutsMillis(RETRANS_TIMEOUT_MS_LIST)
+                        .build();
+
+        verifyIkeParamsMinimum(sessionParams);
+        assertArrayEquals(RETRANS_TIMEOUT_MS_LIST, sessionParams.getRetransmissionTimeoutsMillis());
+    }
+
+    @Test
+    public void testSetPcscfConfigRequests() throws Exception {
+        IkeSessionParams sessionParams =
+                createIkeParamsBuilderMinimum()
+                        .setRetransmissionTimeoutsMillis(RETRANS_TIMEOUT_MS_LIST)
+                        .addPcscfServerRequest(AF_INET)
+                        .addPcscfServerRequest(PCSCF_IPV4_ADDRESS_1)
+                        .addPcscfServerRequest(PCSCF_IPV6_ADDRESS_1)
+                        .addPcscfServerRequest(AF_INET6)
+                        .addPcscfServerRequest(PCSCF_IPV4_ADDRESS_2)
+                        .addPcscfServerRequest(PCSCF_IPV6_ADDRESS_2)
+                        .build();
+
+        verifyIkeParamsMinimum(sessionParams);
+        verifyConfigRequestTypes(EXPECTED_REQ_COUNT, sessionParams.getConfigurationRequests());
+
+        Set<InetAddress> resultAddresses = new HashSet<>();
+        for (IkeConfigRequest req : sessionParams.getConfigurationRequests()) {
+            if (req instanceof ConfigRequestIpv4PcscfServer
+                    && ((ConfigRequestIpv4PcscfServer) req).getAddress() != null) {
+                resultAddresses.add(((ConfigRequestIpv4PcscfServer) req).getAddress());
+            } else if (req instanceof ConfigRequestIpv6PcscfServer
+                    && ((ConfigRequestIpv6PcscfServer) req).getAddress() != null) {
+                resultAddresses.add(((ConfigRequestIpv6PcscfServer) req).getAddress());
+            }
+        }
+        assertEquals(EXPECTED_PCSCF_SERVERS, resultAddresses);
+    }
+
+    @Test
+    public void testAddIkeOption() throws Exception {
+        IkeSessionParams sessionParams =
+                createIkeParamsBuilderMinimum()
+                        .addIkeOption(IKE_OPTION_ACCEPT_ANY_REMOTE_ID)
+                        .build();
+
+        verifyIkeParamsMinimum(sessionParams);
+        assertTrue(sessionParams.hasIkeOption(IKE_OPTION_ACCEPT_ANY_REMOTE_ID));
+    }
+
+    @Test
+    public void testRemoveIkeOption() throws Exception {
+        IkeSessionParams sessionParams =
+                createIkeParamsBuilderMinimum()
+                        .addIkeOption(IKE_OPTION_ACCEPT_ANY_REMOTE_ID)
+                        .removeIkeOption(IKE_OPTION_ACCEPT_ANY_REMOTE_ID)
+                        .build();
+
+        verifyIkeParamsMinimum(sessionParams);
+        assertFalse(sessionParams.hasIkeOption(IKE_OPTION_ACCEPT_ANY_REMOTE_ID));
+    }
+
+    /**
+     * Create a Builder that has minimum configurations to build an IkeSessionParams, except for
+     * authentication method.
+     */
+    private IkeSessionParams.Builder createIkeParamsBuilderMinimumWithoutAuth() {
+        return new IkeSessionParams.Builder(sContext)
+                .setNetwork(sTunNetwork)
+                .setServerHostname(IPV4_ADDRESS_REMOTE.getHostAddress())
+                .addSaProposal(SA_PROPOSAL)
+                .setLocalIdentification(LOCAL_ID)
+                .setRemoteIdentification(REMOTE_ID);
+    }
+
+    /**
+     * Verify the minimum configurations to build an IkeSessionParams, except for authentication
+     * method.
+     *
+     * @see #createIkeParamsBuilderMinimumWithoutAuth
+     */
+    private void verifyIkeParamsMinimumWithoutAuth(IkeSessionParams sessionParams) {
+        assertEquals(sTunNetwork, sessionParams.getNetwork());
+        assertEquals(IPV4_ADDRESS_REMOTE.getHostAddress(), sessionParams.getServerHostname());
+        assertEquals(Arrays.asList(SA_PROPOSAL), sessionParams.getSaProposals());
+        assertEquals(LOCAL_ID, sessionParams.getLocalIdentification());
+        assertEquals(REMOTE_ID, sessionParams.getRemoteIdentification());
+    }
+
+    @Test
+    public void testBuildWithPsk() throws Exception {
+        IkeSessionParams sessionParams =
+                createIkeParamsBuilderMinimumWithoutAuth().setAuthPsk(IKE_PSK).build();
+
+        verifyIkeParamsMinimumWithoutAuth(sessionParams);
+
+        IkeAuthConfig localConfig = sessionParams.getLocalAuthConfig();
+        assertTrue(localConfig instanceof IkeAuthPskConfig);
+        assertArrayEquals(IKE_PSK, ((IkeAuthPskConfig) localConfig).getPsk());
+        IkeAuthConfig remoteConfig = sessionParams.getRemoteAuthConfig();
+        assertTrue(remoteConfig instanceof IkeAuthPskConfig);
+        assertArrayEquals(IKE_PSK, ((IkeAuthPskConfig) remoteConfig).getPsk());
+    }
+
+    @Test
+    public void testBuildWithEap() throws Exception {
+        IkeSessionParams sessionParams =
+                createIkeParamsBuilderMinimumWithoutAuth()
+                        .setAuthEap(mServerCaCert, EAP_ALL_METHODS_CONFIG)
+                        .build();
+
+        verifyIkeParamsMinimumWithoutAuth(sessionParams);
+
+        IkeAuthConfig localConfig = sessionParams.getLocalAuthConfig();
+        assertTrue(localConfig instanceof IkeAuthEapConfig);
+        assertEquals(EAP_ALL_METHODS_CONFIG, ((IkeAuthEapConfig) localConfig).getEapConfig());
+        IkeAuthConfig remoteConfig = sessionParams.getRemoteAuthConfig();
+        assertTrue(remoteConfig instanceof IkeAuthDigitalSignRemoteConfig);
+        assertEquals(
+                mServerCaCert, ((IkeAuthDigitalSignRemoteConfig) remoteConfig).getRemoteCaCert());
+    }
+
+    @Test
+    public void testBuildWithEapOnlyAuth() throws Exception {
+        IkeSessionParams sessionParams =
+                createIkeParamsBuilderMinimumWithoutAuth()
+                        .setAuthEap(mServerCaCert, EAP_ONLY_SAFE_METHODS_CONFIG)
+                        .addIkeOption(IKE_OPTION_EAP_ONLY_AUTH)
+                        .build();
+
+        assertTrue(sessionParams.hasIkeOption(IKE_OPTION_EAP_ONLY_AUTH));
+        verifyIkeParamsMinimumWithoutAuth(sessionParams);
+
+        IkeAuthConfig localConfig = sessionParams.getLocalAuthConfig();
+        assertTrue(localConfig instanceof IkeAuthEapConfig);
+        assertEquals(EAP_ONLY_SAFE_METHODS_CONFIG, ((IkeAuthEapConfig) localConfig).getEapConfig());
+        IkeAuthConfig remoteConfig = sessionParams.getRemoteAuthConfig();
+        assertTrue(remoteConfig instanceof IkeAuthDigitalSignRemoteConfig);
+        assertEquals(
+                mServerCaCert, ((IkeAuthDigitalSignRemoteConfig) remoteConfig).getRemoteCaCert());
+    }
+
+    @Test
+    public void testThrowBuildEapOnlyAuthWithUnsafeMethod() throws Exception {
+        try {
+            IkeSessionParams sessionParams =
+                    createIkeParamsBuilderMinimumWithoutAuth()
+                            .setAuthEap(mServerCaCert, EAP_ALL_METHODS_CONFIG)
+                            .addIkeOption(IKE_OPTION_EAP_ONLY_AUTH)
+                            .build();
+            fail("Expected to fail because EAP only unsafe method is proposed");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testBuildWithDigitalSignature() throws Exception {
+        IkeSessionParams sessionParams =
+                createIkeParamsBuilderMinimumWithoutAuth()
+                        .setAuthDigitalSignature(mServerCaCert, mClientEndCert, mClientPrivateKey)
+                        .build();
+
+        verifyIkeParamsMinimumWithoutAuth(sessionParams);
+
+        IkeAuthConfig localConfig = sessionParams.getLocalAuthConfig();
+        assertTrue(localConfig instanceof IkeAuthDigitalSignLocalConfig);
+        IkeAuthDigitalSignLocalConfig localSignConfig = (IkeAuthDigitalSignLocalConfig) localConfig;
+        assertEquals(mClientEndCert, localSignConfig.getClientEndCertificate());
+        assertEquals(Collections.EMPTY_LIST, localSignConfig.getIntermediateCertificates());
+        assertEquals(mClientPrivateKey, localSignConfig.getPrivateKey());
+
+        IkeAuthConfig remoteConfig = sessionParams.getRemoteAuthConfig();
+        assertTrue(remoteConfig instanceof IkeAuthDigitalSignRemoteConfig);
+        assertEquals(
+                mServerCaCert, ((IkeAuthDigitalSignRemoteConfig) remoteConfig).getRemoteCaCert());
+    }
+
+    @Test
+    public void testBuildWithDigitalSignatureAndIntermediateCerts() throws Exception {
+        List<X509Certificate> intermediateCerts =
+                Arrays.asList(mClientIntermediateCaCertOne, mClientIntermediateCaCertTwo);
+
+        IkeSessionParams sessionParams =
+                createIkeParamsBuilderMinimumWithoutAuth()
+                        .setAuthDigitalSignature(
+                                mServerCaCert, mClientEndCert, intermediateCerts, mClientPrivateKey)
+                        .build();
+
+        verifyIkeParamsMinimumWithoutAuth(sessionParams);
+
+        IkeAuthConfig localConfig = sessionParams.getLocalAuthConfig();
+        assertTrue(localConfig instanceof IkeAuthDigitalSignLocalConfig);
+        IkeAuthDigitalSignLocalConfig localSignConfig = (IkeAuthDigitalSignLocalConfig) localConfig;
+        assertEquals(mClientEndCert, localSignConfig.getClientEndCertificate());
+        assertEquals(intermediateCerts, localSignConfig.getIntermediateCertificates());
+        assertEquals(mClientPrivateKey, localSignConfig.getPrivateKey());
+
+        IkeAuthConfig remoteConfig = sessionParams.getRemoteAuthConfig();
+        assertTrue(remoteConfig instanceof IkeAuthDigitalSignRemoteConfig);
+        assertEquals(
+                mServerCaCert, ((IkeAuthDigitalSignRemoteConfig) remoteConfig).getRemoteCaCert());
+    }
+}
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionParamsTestBase.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionParamsTestBase.java
new file mode 100644
index 0000000..c3e3ba3
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionParamsTestBase.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.ipsec.ike.cts;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.ipsec.ike.cts.TestNetworkUtils.TestNetworkCallback;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "MANAGE_TEST_NETWORKS permission can't be granted to instant apps")
+abstract class IkeSessionParamsTestBase extends IkeTestBase {
+    // Static state to reduce setup/teardown
+    static ConnectivityManager sCM;
+    static TestNetworkManager sTNM;
+    static ParcelFileDescriptor sTunFd;
+    static TestNetworkCallback sTunNetworkCallback;
+    static Network sTunNetwork;
+
+    static Context sContext = InstrumentationRegistry.getContext();
+    static IBinder sBinder = new Binder();
+
+    // This method is guaranteed to run in subclasses and will run before subclasses' @BeforeClass
+    // methods.
+    @BeforeClass
+    public static void setUpTestNetworkBeforeClass() throws Exception {
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity();
+        sCM = (ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+        sTNM = (TestNetworkManager) sContext.getSystemService(Context.TEST_NETWORK_SERVICE);
+
+        TestNetworkInterface testIface =
+                sTNM.createTunInterface(
+                        new LinkAddress[] {new LinkAddress(IPV4_ADDRESS_LOCAL, IP4_PREFIX_LEN)});
+
+        sTunFd = testIface.getFileDescriptor();
+        sTunNetworkCallback =
+                TestNetworkUtils.setupAndGetTestNetwork(
+                        sCM, sTNM, testIface.getInterfaceName(), sBinder);
+        sTunNetwork = sTunNetworkCallback.getNetworkBlocking();
+    }
+
+    // This method is guaranteed to run in subclasses and will run after subclasses' @AfterClass
+    // methods.
+    @AfterClass
+    public static void tearDownTestNetworkAfterClass() throws Exception {
+        sCM.unregisterNetworkCallback(sTunNetworkCallback);
+
+        sTNM.teardownTestNetwork(sTunNetwork);
+        sTunFd.close();
+
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+}
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTestBase.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTestBase.java
index d3aa8d0..bc2bec6 100644
--- a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTestBase.java
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTestBase.java
@@ -24,6 +24,7 @@
 
 import java.net.Inet4Address;
 import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -40,6 +41,20 @@
     static final int IP4_PREFIX_LEN = 32;
     static final int IP6_PREFIX_LEN = 64;
 
+    static final byte[] IKE_PSK = "ikeAndroidPsk".getBytes();
+
+    static final String LOCAL_HOSTNAME = "client.test.ike.android.net";
+    static final String REMOTE_HOSTNAME = "server.test.ike.android.net";
+    static final String LOCAL_ASN1_DN_STRING = "CN=client.test.ike.android.net, O=Android, C=US";
+    static final String LOCAL_RFC822_NAME = "client.test.ike@example.com";
+    static final byte[] LOCAL_KEY_ID = "Local Key ID".getBytes();
+
+    static final int SUB_ID = 1;
+    static final byte[] EAP_IDENTITY = "test@android.net".getBytes();
+    static final String NETWORK_NAME = "android.net";
+    static final String EAP_MSCHAPV2_USERNAME = "username";
+    static final String EAP_MSCHAPV2_PASSWORD = "password";
+
     static final Inet4Address IPV4_ADDRESS_LOCAL =
             (Inet4Address) (InetAddresses.parseNumericAddress("192.0.2.100"));
     static final Inet4Address IPV4_ADDRESS_REMOTE =
@@ -49,6 +64,13 @@
     static final Inet6Address IPV6_ADDRESS_REMOTE =
             (Inet6Address) (InetAddresses.parseNumericAddress("2001:db8:255::100"));
 
+    static final InetAddress PCSCF_IPV4_ADDRESS_1 = InetAddresses.parseNumericAddress("192.0.2.1");
+    static final InetAddress PCSCF_IPV4_ADDRESS_2 = InetAddresses.parseNumericAddress("192.0.2.2");
+    static final InetAddress PCSCF_IPV6_ADDRESS_1 =
+            InetAddresses.parseNumericAddress("2001:DB8::1");
+    static final InetAddress PCSCF_IPV6_ADDRESS_2 =
+            InetAddresses.parseNumericAddress("2001:DB8::2");
+
     static final IkeTrafficSelector DEFAULT_V4_TS =
             new IkeTrafficSelector(
                     MIN_PORT,
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/PacketUtils.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/PacketUtils.java
new file mode 100644
index 0000000..35e6719
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/PacketUtils.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.ipsec.ike.cts;
+
+import static android.system.OsConstants.IPPROTO_IPV6;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * This code is a exact copy of {@link PacketUtils} in
+ * cts/tests/tests/net/src/android/net/cts/PacketUtils.java.
+ *
+ * <p>TODO(b/148689509): Statically include the PacketUtils source file instead of copying it.
+ */
+public class PacketUtils {
+    private static final String TAG = PacketUtils.class.getSimpleName();
+
+    private static final int DATA_BUFFER_LEN = 4096;
+
+    static final int IP4_HDRLEN = 20;
+    static final int IP6_HDRLEN = 40;
+    static final int UDP_HDRLEN = 8;
+    static final int TCP_HDRLEN = 20;
+    static final int TCP_HDRLEN_WITH_TIMESTAMP_OPT = TCP_HDRLEN + 12;
+
+    // Not defined in OsConstants
+    static final int IPPROTO_IPV4 = 4;
+    static final int IPPROTO_ESP = 50;
+
+    // Encryption parameters
+    static final int AES_GCM_IV_LEN = 8;
+    static final int AES_CBC_IV_LEN = 16;
+    static final int AES_GCM_BLK_SIZE = 4;
+    static final int AES_CBC_BLK_SIZE = 16;
+
+    // Encryption algorithms
+    static final String AES = "AES";
+    static final String AES_CBC = "AES/CBC/NoPadding";
+    static final String HMAC_SHA_256 = "HmacSHA256";
+
+    public interface Payload {
+        byte[] getPacketBytes(IpHeader header) throws Exception;
+
+        void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) throws Exception;
+
+        short length();
+
+        int getProtocolId();
+    }
+
+    public abstract static class IpHeader {
+
+        public final byte proto;
+        public final InetAddress srcAddr;
+        public final InetAddress dstAddr;
+        public final Payload payload;
+
+        public IpHeader(int proto, InetAddress src, InetAddress dst, Payload payload) {
+            this.proto = (byte) proto;
+            this.srcAddr = src;
+            this.dstAddr = dst;
+            this.payload = payload;
+        }
+
+        public abstract byte[] getPacketBytes() throws Exception;
+
+        public abstract int getProtocolId();
+    }
+
+    public static class Ip4Header extends IpHeader {
+        private short checksum;
+
+        public Ip4Header(int proto, Inet4Address src, Inet4Address dst, Payload payload) {
+            super(proto, src, dst, payload);
+        }
+
+        public byte[] getPacketBytes() throws Exception {
+            ByteBuffer resultBuffer = buildHeader();
+            payload.addPacketBytes(this, resultBuffer);
+
+            return getByteArrayFromBuffer(resultBuffer);
+        }
+
+        public ByteBuffer buildHeader() {
+            ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+            // Version, IHL
+            bb.put((byte) (0x45));
+
+            // DCSP, ECN
+            bb.put((byte) 0);
+
+            // Total Length
+            bb.putShort((short) (IP4_HDRLEN + payload.length()));
+
+            // Empty for Identification, Flags and Fragment Offset
+            bb.putShort((short) 0);
+            bb.put((byte) 0x40);
+            bb.put((byte) 0x00);
+
+            // TTL
+            bb.put((byte) 64);
+
+            // Protocol
+            bb.put(proto);
+
+            // Header Checksum
+            final int ipChecksumOffset = bb.position();
+            bb.putShort((short) 0);
+
+            // Src/Dst addresses
+            bb.put(srcAddr.getAddress());
+            bb.put(dstAddr.getAddress());
+
+            bb.putShort(ipChecksumOffset, calculateChecksum(bb));
+
+            return bb;
+        }
+
+        private short calculateChecksum(ByteBuffer bb) {
+            int checksum = 0;
+
+            // Calculate sum of 16-bit values, excluding checksum. IPv4 headers are always 32-bit
+            // aligned, so no special cases needed for unaligned values.
+            ShortBuffer shortBuffer = ByteBuffer.wrap(getByteArrayFromBuffer(bb)).asShortBuffer();
+            while (shortBuffer.hasRemaining()) {
+                short val = shortBuffer.get();
+
+                // Wrap as needed
+                checksum = addAndWrapForChecksum(checksum, val);
+            }
+
+            return onesComplement(checksum);
+        }
+
+        public int getProtocolId() {
+            return IPPROTO_IPV4;
+        }
+    }
+
+    public static class Ip6Header extends IpHeader {
+        public Ip6Header(int nextHeader, Inet6Address src, Inet6Address dst, Payload payload) {
+            super(nextHeader, src, dst, payload);
+        }
+
+        public byte[] getPacketBytes() throws Exception {
+            ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+            // Version | Traffic Class (First 4 bits)
+            bb.put((byte) 0x60);
+
+            // Traffic class (Last 4 bits), Flow Label
+            bb.put((byte) 0);
+            bb.put((byte) 0);
+            bb.put((byte) 0);
+
+            // Payload Length
+            bb.putShort((short) payload.length());
+
+            // Next Header
+            bb.put(proto);
+
+            // Hop Limit
+            bb.put((byte) 64);
+
+            // Src/Dst addresses
+            bb.put(srcAddr.getAddress());
+            bb.put(dstAddr.getAddress());
+
+            // Payload
+            payload.addPacketBytes(this, bb);
+
+            return getByteArrayFromBuffer(bb);
+        }
+
+        public int getProtocolId() {
+            return IPPROTO_IPV6;
+        }
+    }
+
+    public static class BytePayload implements Payload {
+        public final byte[] payload;
+
+        public BytePayload(byte[] payload) {
+            this.payload = payload;
+        }
+
+        public int getProtocolId() {
+            return -1;
+        }
+
+        public byte[] getPacketBytes(IpHeader header) {
+            ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+            addPacketBytes(header, bb);
+            return getByteArrayFromBuffer(bb);
+        }
+
+        public void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) {
+            resultBuffer.put(payload);
+        }
+
+        public short length() {
+            return (short) payload.length;
+        }
+    }
+
+    public static class UdpHeader implements Payload {
+
+        public final short srcPort;
+        public final short dstPort;
+        public final Payload payload;
+
+        public UdpHeader(int srcPort, int dstPort, Payload payload) {
+            this.srcPort = (short) srcPort;
+            this.dstPort = (short) dstPort;
+            this.payload = payload;
+        }
+
+        public int getProtocolId() {
+            return IPPROTO_UDP;
+        }
+
+        public short length() {
+            return (short) (payload.length() + 8);
+        }
+
+        public byte[] getPacketBytes(IpHeader header) throws Exception {
+            ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+            addPacketBytes(header, bb);
+            return getByteArrayFromBuffer(bb);
+        }
+
+        public void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) throws Exception {
+            // Source, Destination port
+            resultBuffer.putShort(srcPort);
+            resultBuffer.putShort(dstPort);
+
+            // Payload Length
+            resultBuffer.putShort(length());
+
+            // Get payload bytes for checksum + payload
+            ByteBuffer payloadBuffer = ByteBuffer.allocate(DATA_BUFFER_LEN);
+            payload.addPacketBytes(header, payloadBuffer);
+            byte[] payloadBytes = getByteArrayFromBuffer(payloadBuffer);
+
+            // Checksum
+            resultBuffer.putShort(calculateChecksum(header, payloadBytes));
+
+            // Payload
+            resultBuffer.put(payloadBytes);
+        }
+
+        private short calculateChecksum(IpHeader header, byte[] payloadBytes) throws Exception {
+            int newChecksum = 0;
+            ShortBuffer srcBuffer = ByteBuffer.wrap(header.srcAddr.getAddress()).asShortBuffer();
+            ShortBuffer dstBuffer = ByteBuffer.wrap(header.dstAddr.getAddress()).asShortBuffer();
+
+            while (srcBuffer.hasRemaining() || dstBuffer.hasRemaining()) {
+                short val = srcBuffer.hasRemaining() ? srcBuffer.get() : dstBuffer.get();
+
+                // Wrap as needed
+                newChecksum = addAndWrapForChecksum(newChecksum, val);
+            }
+
+            // Add pseudo-header values. Proto is 0-padded, so just use the byte.
+            newChecksum = addAndWrapForChecksum(newChecksum, header.proto);
+            newChecksum = addAndWrapForChecksum(newChecksum, length());
+            newChecksum = addAndWrapForChecksum(newChecksum, srcPort);
+            newChecksum = addAndWrapForChecksum(newChecksum, dstPort);
+            newChecksum = addAndWrapForChecksum(newChecksum, length());
+
+            ShortBuffer payloadShortBuffer = ByteBuffer.wrap(payloadBytes).asShortBuffer();
+            while (payloadShortBuffer.hasRemaining()) {
+                newChecksum = addAndWrapForChecksum(newChecksum, payloadShortBuffer.get());
+            }
+            if (payload.length() % 2 != 0) {
+                newChecksum =
+                        addAndWrapForChecksum(
+                                newChecksum, (payloadBytes[payloadBytes.length - 1] << 8));
+            }
+
+            return onesComplement(newChecksum);
+        }
+    }
+
+    public static class EspHeader implements Payload {
+        public final int nextHeader;
+        public final int spi;
+        public final int seqNum;
+        public final byte[] key;
+        public final byte[] payload;
+
+        /**
+         * Generic constructor for ESP headers.
+         *
+         * <p>For Tunnel mode, payload will be a full IP header + attached payloads
+         *
+         * <p>For Transport mode, payload will be only the attached payloads, but with the checksum
+         * calculated using the pre-encryption IP header
+         */
+        public EspHeader(int nextHeader, int spi, int seqNum, byte[] key, byte[] payload) {
+            this.nextHeader = nextHeader;
+            this.spi = spi;
+            this.seqNum = seqNum;
+            this.key = key;
+            this.payload = payload;
+        }
+
+        public int getProtocolId() {
+            return IPPROTO_ESP;
+        }
+
+        public short length() {
+            // ALWAYS uses AES-CBC, HMAC-SHA256 (128b trunc len)
+            return (short)
+                    calculateEspPacketSize(payload.length, AES_CBC_IV_LEN, AES_CBC_BLK_SIZE, 128);
+        }
+
+        public byte[] getPacketBytes(IpHeader header) throws Exception {
+            ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN);
+
+            addPacketBytes(header, bb);
+            return getByteArrayFromBuffer(bb);
+        }
+
+        public void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) throws Exception {
+            ByteBuffer espPayloadBuffer = ByteBuffer.allocate(DATA_BUFFER_LEN);
+            espPayloadBuffer.putInt(spi);
+            espPayloadBuffer.putInt(seqNum);
+            espPayloadBuffer.put(getCiphertext(key));
+
+            espPayloadBuffer.put(getIcv(getByteArrayFromBuffer(espPayloadBuffer)), 0, 16);
+            resultBuffer.put(getByteArrayFromBuffer(espPayloadBuffer));
+        }
+
+        private byte[] getIcv(byte[] authenticatedSection) throws GeneralSecurityException {
+            Mac sha256HMAC = Mac.getInstance(HMAC_SHA_256);
+            SecretKeySpec authKey = new SecretKeySpec(key, HMAC_SHA_256);
+            sha256HMAC.init(authKey);
+
+            return sha256HMAC.doFinal(authenticatedSection);
+        }
+
+        /**
+         * Encrypts and builds ciphertext block. Includes the IV, Padding and Next-Header blocks
+         *
+         * <p>The ciphertext does NOT include the SPI/Sequence numbers, or the ICV.
+         */
+        private byte[] getCiphertext(byte[] key) throws GeneralSecurityException {
+            int paddedLen = calculateEspEncryptedLength(payload.length, AES_CBC_BLK_SIZE);
+            ByteBuffer paddedPayload = ByteBuffer.allocate(paddedLen);
+            paddedPayload.put(payload);
+
+            // Add padding - consecutive integers from 0x01
+            int pad = 1;
+            while (paddedPayload.position() < paddedPayload.limit()) {
+                paddedPayload.put((byte) pad++);
+            }
+
+            paddedPayload.position(paddedPayload.limit() - 2);
+            paddedPayload.put((byte) (paddedLen - 2 - payload.length)); // Pad length
+            paddedPayload.put((byte) nextHeader);
+
+            // Generate Initialization Vector
+            byte[] iv = new byte[AES_CBC_IV_LEN];
+            new SecureRandom().nextBytes(iv);
+            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
+            SecretKeySpec secretKeySpec = new SecretKeySpec(key, AES);
+
+            // Encrypt payload
+            Cipher cipher = Cipher.getInstance(AES_CBC);
+            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
+            byte[] encrypted = cipher.doFinal(getByteArrayFromBuffer(paddedPayload));
+
+            // Build ciphertext
+            ByteBuffer cipherText = ByteBuffer.allocate(AES_CBC_IV_LEN + encrypted.length);
+            cipherText.put(iv);
+            cipherText.put(encrypted);
+
+            return getByteArrayFromBuffer(cipherText);
+        }
+    }
+
+    private static int addAndWrapForChecksum(int currentChecksum, int value) {
+        currentChecksum += value & 0x0000ffff;
+
+        // Wrap anything beyond the first 16 bits, and add to lower order bits
+        return (currentChecksum >>> 16) + (currentChecksum & 0x0000ffff);
+    }
+
+    private static short onesComplement(int val) {
+        val = (val >>> 16) + (val & 0xffff);
+
+        if (val == 0) return 0;
+        return (short) ((~val) & 0xffff);
+    }
+
+    public static int calculateEspPacketSize(
+            int payloadLen, int cryptIvLength, int cryptBlockSize, int authTruncLen) {
+        final int ESP_HDRLEN = 4 + 4; // SPI + Seq#
+        final int ICV_LEN = authTruncLen / 8; // Auth trailer; based on truncation length
+        payloadLen += cryptIvLength; // Initialization Vector
+
+        // Align to block size of encryption algorithm
+        payloadLen = calculateEspEncryptedLength(payloadLen, cryptBlockSize);
+        return payloadLen + ESP_HDRLEN + ICV_LEN;
+    }
+
+    private static int calculateEspEncryptedLength(int payloadLen, int cryptBlockSize) {
+        payloadLen += 2; // ESP trailer
+
+        // Align to block size of encryption algorithm
+        return payloadLen + calculateEspPadLen(payloadLen, cryptBlockSize);
+    }
+
+    private static int calculateEspPadLen(int payloadLen, int cryptBlockSize) {
+        return (cryptBlockSize - (payloadLen % cryptBlockSize)) % cryptBlockSize;
+    }
+
+    private static byte[] getByteArrayFromBuffer(ByteBuffer buffer) {
+        return Arrays.copyOfRange(buffer.array(), 0, buffer.position());
+    }
+
+    /*
+     * Debug printing
+     */
+    private static final char[] hexArray = "0123456789ABCDEF".toCharArray();
+
+    public static String bytesToHex(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(hexArray[b >>> 4]);
+            sb.append(hexArray[b & 0x0F]);
+            sb.append(' ');
+        }
+        return sb.toString();
+    }
+}
diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/TunUtils.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/TunUtils.java
new file mode 100644
index 0000000..71450ea
--- /dev/null
+++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/TunUtils.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.ipsec.ike.cts;
+
+import static android.net.ipsec.ike.cts.PacketUtils.IP4_HDRLEN;
+import static android.net.ipsec.ike.cts.PacketUtils.IP6_HDRLEN;
+import static android.net.ipsec.ike.cts.PacketUtils.IPPROTO_ESP;
+import static android.net.ipsec.ike.cts.PacketUtils.UDP_HDRLEN;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import android.os.ParcelFileDescriptor;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * This code is a exact copy of {@link TunUtils} in
+ * cts/tests/tests/net/src/android/net/cts/TunUtils.java, except the import path of PacketUtils is
+ * the path to the copy of PacktUtils.
+ *
+ * <p>TODO(b/148689509): Statically include the TunUtils source file instead of copying it.
+ */
+public class TunUtils {
+    private static final String TAG = TunUtils.class.getSimpleName();
+
+    private static final int DATA_BUFFER_LEN = 4096;
+    private static final int TIMEOUT = 100;
+
+    private static final int IP4_PROTO_OFFSET = 9;
+    private static final int IP6_PROTO_OFFSET = 6;
+
+    private static final int IP4_ADDR_OFFSET = 12;
+    private static final int IP4_ADDR_LEN = 4;
+    private static final int IP6_ADDR_OFFSET = 8;
+    private static final int IP6_ADDR_LEN = 16;
+
+    private final ParcelFileDescriptor mTunFd;
+    private final List<byte[]> mPackets = new ArrayList<>();
+    private final Thread mReaderThread;
+
+    public TunUtils(ParcelFileDescriptor tunFd) {
+        mTunFd = tunFd;
+
+        // Start background reader thread
+        mReaderThread =
+                new Thread(
+                        () -> {
+                            try {
+                                // Loop will exit and thread will quit when tunFd is closed.
+                                // Receiving either EOF or an exception will exit this reader loop.
+                                // FileInputStream in uninterruptable, so there's no good way to
+                                // ensure that this thread shuts down except upon FD closure.
+                                while (true) {
+                                    byte[] intercepted = receiveFromTun();
+                                    if (intercepted == null) {
+                                        // Exit once we've hit EOF
+                                        return;
+                                    } else if (intercepted.length > 0) {
+                                        // Only save packet if we've received any bytes.
+                                        synchronized (mPackets) {
+                                            mPackets.add(intercepted);
+                                            mPackets.notifyAll();
+                                        }
+                                    }
+                                }
+                            } catch (IOException ignored) {
+                                // Simply exit this reader thread
+                                return;
+                            }
+                        });
+        mReaderThread.start();
+    }
+
+    private byte[] receiveFromTun() throws IOException {
+        FileInputStream in = new FileInputStream(mTunFd.getFileDescriptor());
+        byte[] inBytes = new byte[DATA_BUFFER_LEN];
+        int bytesRead = in.read(inBytes);
+
+        if (bytesRead < 0) {
+            return null; // return null for EOF
+        } else if (bytesRead >= DATA_BUFFER_LEN) {
+            throw new IllegalStateException("Too big packet. Fragmentation unsupported");
+        }
+        return Arrays.copyOf(inBytes, bytesRead);
+    }
+
+    private byte[] getFirstMatchingPacket(Predicate<byte[]> verifier, int startIndex) {
+        synchronized (mPackets) {
+            for (int i = startIndex; i < mPackets.size(); i++) {
+                byte[] pkt = mPackets.get(i);
+                if (verifier.test(pkt)) {
+                    return pkt;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Checks if the specified bytes were ever sent in plaintext.
+     *
+     * <p>Only checks for known plaintext bytes to prevent triggering on ICMP/RA packets or the like
+     *
+     * @param plaintext the plaintext bytes to check for
+     * @param startIndex the index in the list to check for
+     */
+    public boolean hasPlaintextPacket(byte[] plaintext, int startIndex) {
+        Predicate<byte[]> verifier =
+                (pkt) -> {
+                    return Collections.indexOfSubList(Arrays.asList(pkt), Arrays.asList(plaintext))
+                            != -1;
+                };
+        return getFirstMatchingPacket(verifier, startIndex) != null;
+    }
+
+    public byte[] getEspPacket(int spi, boolean encap, int startIndex) {
+        return getFirstMatchingPacket(
+                (pkt) -> {
+                    return isEsp(pkt, spi, encap);
+                },
+                startIndex);
+    }
+
+    public byte[] awaitEspPacketNoPlaintext(
+            int spi, byte[] plaintext, boolean useEncap, int expectedPacketSize) throws Exception {
+        long endTime = System.currentTimeMillis() + TIMEOUT;
+        int startIndex = 0;
+
+        synchronized (mPackets) {
+            while (System.currentTimeMillis() < endTime) {
+                byte[] espPkt = getEspPacket(spi, useEncap, startIndex);
+                if (espPkt != null) {
+                    // Validate packet size
+                    assertEquals(expectedPacketSize, espPkt.length);
+
+                    // Always check plaintext from start
+                    assertFalse(hasPlaintextPacket(plaintext, 0));
+                    return espPkt; // We've found the packet we're looking for.
+                }
+
+                startIndex = mPackets.size();
+
+                // Try to prevent waiting too long. If waitTimeout <= 0, we've already hit timeout
+                long waitTimeout = endTime - System.currentTimeMillis();
+                if (waitTimeout > 0) {
+                    mPackets.wait(waitTimeout);
+                }
+            }
+
+            fail("No such ESP packet found with SPI " + spi);
+        }
+        return null;
+    }
+
+    private static boolean isSpiEqual(byte[] pkt, int espOffset, int spi) {
+        // Check SPI byte by byte.
+        return pkt[espOffset] == (byte) ((spi >>> 24) & 0xff)
+                && pkt[espOffset + 1] == (byte) ((spi >>> 16) & 0xff)
+                && pkt[espOffset + 2] == (byte) ((spi >>> 8) & 0xff)
+                && pkt[espOffset + 3] == (byte) (spi & 0xff);
+    }
+
+    private static boolean isEsp(byte[] pkt, int spi, boolean encap) {
+        if (isIpv6(pkt)) {
+            // IPv6 UDP encap not supported by kernels; assume non-encap.
+            return pkt[IP6_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP6_HDRLEN, spi);
+        } else {
+            // Use default IPv4 header length (assuming no options)
+            if (encap) {
+                return pkt[IP4_PROTO_OFFSET] == IPPROTO_UDP
+                        && isSpiEqual(pkt, IP4_HDRLEN + UDP_HDRLEN, spi);
+            } else {
+                return pkt[IP4_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP4_HDRLEN, spi);
+            }
+        }
+    }
+
+    private static boolean isIpv6(byte[] pkt) {
+        // First nibble shows IP version. 0x60 for IPv6
+        return (pkt[0] & (byte) 0xF0) == (byte) 0x60;
+    }
+
+    private static byte[] getReflectedPacket(byte[] pkt) {
+        byte[] reflected = Arrays.copyOf(pkt, pkt.length);
+
+        if (isIpv6(pkt)) {
+            // Set reflected packet's dst to that of the original's src
+            System.arraycopy(
+                    pkt, // src
+                    IP6_ADDR_OFFSET + IP6_ADDR_LEN, // src offset
+                    reflected, // dst
+                    IP6_ADDR_OFFSET, // dst offset
+                    IP6_ADDR_LEN); // len
+            // Set reflected packet's src IP to that of the original's dst IP
+            System.arraycopy(
+                    pkt, // src
+                    IP6_ADDR_OFFSET, // src offset
+                    reflected, // dst
+                    IP6_ADDR_OFFSET + IP6_ADDR_LEN, // dst offset
+                    IP6_ADDR_LEN); // len
+        } else {
+            // Set reflected packet's dst to that of the original's src
+            System.arraycopy(
+                    pkt, // src
+                    IP4_ADDR_OFFSET + IP4_ADDR_LEN, // src offset
+                    reflected, // dst
+                    IP4_ADDR_OFFSET, // dst offset
+                    IP4_ADDR_LEN); // len
+            // Set reflected packet's src IP to that of the original's dst IP
+            System.arraycopy(
+                    pkt, // src
+                    IP4_ADDR_OFFSET, // src offset
+                    reflected, // dst
+                    IP4_ADDR_OFFSET + IP4_ADDR_LEN, // dst offset
+                    IP4_ADDR_LEN); // len
+        }
+        return reflected;
+    }
+
+    /** Takes all captured packets, flips the src/dst, and re-injects them. */
+    public void reflectPackets() throws IOException {
+        synchronized (mPackets) {
+            for (byte[] pkt : mPackets) {
+                injectPacket(getReflectedPacket(pkt));
+            }
+        }
+    }
+
+    public void injectPacket(byte[] pkt) throws IOException {
+        FileOutputStream out = new FileOutputStream(mTunFd.getFileDescriptor());
+        out.write(pkt);
+        out.flush();
+    }
+
+    /** Resets the intercepted packets. */
+    public void reset() throws IOException {
+        synchronized (mPackets) {
+            mPackets.clear();
+        }
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 3a52ee6..1ee08ff 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -639,11 +639,14 @@
         }
     }
 
-    private void waitForActiveNetworkMetered(boolean requestedMeteredness) throws Exception {
+    private void waitForActiveNetworkMetered(int targetTransportType, boolean requestedMeteredness)
+            throws Exception {
         final CountDownLatch latch = new CountDownLatch(1);
         final NetworkCallback networkCallback = new NetworkCallback() {
             @Override
             public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
+                if (!nc.hasTransport(targetTransportType)) return;
+
                 final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED);
                 if (metered == requestedMeteredness) {
                     latch.countDown();
@@ -720,10 +723,10 @@
             Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE,
                     Integer.toString(newMeteredPreference));
             setWifiMeteredStatus(ssid, "true");
-            waitForActiveNetworkMetered(true);
+            waitForActiveNetworkMetered(TRANSPORT_WIFI, true);
             // Wifi meterness changes from unmetered to metered will disconnect and reconnect since
             // R.
-            final Network network = mCm.getActiveNetwork();
+            final Network network = ensureWifiConnected();
             assertEquals(ssid, unquoteSSID(mWifiManager.getConnectionInfo().getSSID()));
             assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
                     NET_CAPABILITY_NOT_METERED), false);
@@ -741,7 +744,7 @@
 
             setWifiMeteredStatus(ssid, "false");
             // No disconnect from unmetered to metered.
-            waitForActiveNetworkMetered(false);
+            waitForActiveNetworkMetered(TRANSPORT_WIFI, false);
             assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
                     NET_CAPABILITY_NOT_METERED), true);
             assertMultipathPreferenceIsEventually(network, newMeteredPreference,
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 6a1d9de..5e92b41 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -29,9 +29,11 @@
 import static org.junit.Assert.assertTrue;
 
 import android.net.MacAddress;
+import android.net.MatchAllNetworkSpecifier;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.NetworkSpecifier;
+import android.net.TelephonyNetworkSpecifier;
 import android.net.wifi.WifiNetworkSpecifier;
 import android.os.Build;
 import android.os.Process;
@@ -127,39 +129,54 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testCanBeSatisfiedBy() {
-        final WifiNetworkSpecifier specifier1 = new WifiNetworkSpecifier.Builder()
-                .setSsidPattern(new PatternMatcher(TEST_SSID, PatternMatcher.PATTERN_LITERAL))
-                .setBssidPattern(ARBITRARY_ADDRESS, ARBITRARY_ADDRESS)
+        final TelephonyNetworkSpecifier specifier1 = new TelephonyNetworkSpecifier.Builder()
+                .setSubscriptionId(1234 /* subId */)
                 .build();
-        final WifiNetworkSpecifier specifier2 = new WifiNetworkSpecifier.Builder()
-                .setSsidPattern(new PatternMatcher(OTHER_SSID, PatternMatcher.PATTERN_LITERAL))
-                .setBssidPattern(ARBITRARY_ADDRESS, ARBITRARY_ADDRESS)
+        final TelephonyNetworkSpecifier specifier2 = new TelephonyNetworkSpecifier.Builder()
+                .setSubscriptionId(5678 /* subId */)
                 .build();
         final NetworkCapabilities cap = new NetworkCapabilities()
-                .addTransportType(TRANSPORT_WIFI)
-                .addCapability(NET_CAPABILITY_INTERNET);
-        final NetworkCapabilities capWithSp =
-                new NetworkCapabilities(cap).setNetworkSpecifier(specifier1);
-        final NetworkCapabilities cellCap = new NetworkCapabilities()
                 .addTransportType(TRANSPORT_CELLULAR)
                 .addCapability(NET_CAPABILITY_MMS)
                 .addCapability(NET_CAPABILITY_INTERNET);
-        final NetworkRequest request = new NetworkRequest.Builder()
-                .addTransportType(TRANSPORT_WIFI)
+        final NetworkCapabilities capDualTransport = new NetworkCapabilities(cap)
+                .addTransportType(TRANSPORT_VPN);
+        final NetworkCapabilities capWithSpecifier1 =
+                new NetworkCapabilities(cap).setNetworkSpecifier(specifier1);
+        final NetworkCapabilities capDiffTransportWithSpecifier1 = new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addTransportType(TRANSPORT_VPN)
+                .setNetworkSpecifier(specifier1);
+
+        final NetworkRequest requestWithSpecifier1 = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .setNetworkSpecifier(specifier1)
                 .build();
-        assertFalse(request.canBeSatisfiedBy(null));
-        assertFalse(request.canBeSatisfiedBy(new NetworkCapabilities()));
-        assertTrue(request.canBeSatisfiedBy(cap));
-        assertTrue(request.canBeSatisfiedBy(
-                new NetworkCapabilities(cap).addTransportType(TRANSPORT_VPN)));
-        assertTrue(request.canBeSatisfiedBy(capWithSp));
-        assertFalse(request.canBeSatisfiedBy(
+        assertFalse(requestWithSpecifier1.canBeSatisfiedBy(null));
+        assertFalse(requestWithSpecifier1.canBeSatisfiedBy(new NetworkCapabilities()));
+        assertTrue(requestWithSpecifier1.canBeSatisfiedBy(new NetworkCapabilities(cap)
+                .setNetworkSpecifier(new MatchAllNetworkSpecifier())));
+        assertTrue(requestWithSpecifier1.canBeSatisfiedBy(cap));
+        assertTrue(requestWithSpecifier1.canBeSatisfiedBy(capWithSpecifier1));
+        assertTrue(requestWithSpecifier1.canBeSatisfiedBy(capDualTransport));
+        assertFalse(requestWithSpecifier1.canBeSatisfiedBy(
                 new NetworkCapabilities(cap).setNetworkSpecifier(specifier2)));
-        assertFalse(request.canBeSatisfiedBy(cellCap));
-        assertEquals(request.canBeSatisfiedBy(capWithSp),
-                new NetworkCapabilities(capWithSp).satisfiedByNetworkCapabilities(capWithSp));
+
+        final NetworkRequest request = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build();
+        assertTrue(request.canBeSatisfiedBy(cap));
+        assertTrue(request.canBeSatisfiedBy(capWithSpecifier1));
+        assertTrue(request.canBeSatisfiedBy(
+                new NetworkCapabilities(cap).setNetworkSpecifier(specifier2)));
+        assertFalse(request.canBeSatisfiedBy(capDiffTransportWithSpecifier1));
+        assertTrue(request.canBeSatisfiedBy(capDualTransport));
+
+        assertEquals(requestWithSpecifier1.canBeSatisfiedBy(capWithSpecifier1),
+                new NetworkCapabilities(capWithSpecifier1)
+                    .satisfiedByNetworkCapabilities(capWithSpecifier1));
     }
 
     @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index 63de301..85bb0e0 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -25,8 +25,11 @@
     ],
 
     static_libs: [
+        "TetheringCommonTests",
         "TetheringIntegrationTestsLib",
         "compatibility-device-util-axt",
+        "cts-net-utils",
+        "net-tests-utils",
         "ctstestrunner-axt",
         "junit",
         "junit-params",
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index ccad14c..bbb9403 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -15,39 +15,59 @@
  */
 package android.tethering.test;
 
-import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
-import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
-import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
+import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.TetheringManager.TETHERING_USB;
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
 import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
 
+import android.app.UiAutomation;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
 import android.net.LinkAddress;
 import android.net.Network;
+import android.net.NetworkCapabilities;
 import android.net.TetheredClient;
 import android.net.TetheringManager;
 import android.net.TetheringManager.OnTetheringEntitlementResultListener;
 import android.net.TetheringManager.TetheringEventCallback;
 import android.net.TetheringManager.TetheringInterfaceRegexps;
 import android.net.TetheringManager.TetheringRequest;
+import android.net.cts.util.CtsNetUtils;
+import android.net.wifi.WifiManager;
 import android.os.Bundle;
 import android.os.ResultReceiver;
+import android.telephony.TelephonyManager;
 
 import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.testutils.ArrayTrackRecord;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -58,10 +78,8 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
 import java.util.function.Consumer;
 
 @RunWith(AndroidJUnit4.class)
@@ -69,21 +87,37 @@
 
     private Context mContext;
 
+    private ConnectivityManager mCm;
     private TetheringManager mTM;
+    private WifiManager mWm;
+    private PackageManager mPm;
 
     private TetherChangeReceiver mTetherChangeReceiver;
-
-    private String[] mTetheredList;
+    private CtsNetUtils mCtsNetUtils;
 
     private static final int DEFAULT_TIMEOUT_MS = 60_000;
 
+    private void adoptShellPermissionIdentity() {
+        final UiAutomation uiAutomation =
+                InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity();
+    }
+
+    private void dropShellPermissionIdentity() {
+        final UiAutomation uiAutomation =
+                InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.dropShellPermissionIdentity();
+    }
+
     @Before
     public void setUp() throws Exception {
-        InstrumentationRegistry.getInstrumentation()
-                .getUiAutomation()
-                .adoptShellPermissionIdentity();
+        adoptShellPermissionIdentity();
         mContext = InstrumentationRegistry.getContext();
+        mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
         mTM = (TetheringManager) mContext.getSystemService(Context.TETHERING_SERVICE);
+        mWm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        mPm = mContext.getPackageManager();
+        mCtsNetUtils = new CtsNetUtils(mContext);
         mTetherChangeReceiver = new TetherChangeReceiver();
         final IntentFilter filter = new IntentFilter(
                 TetheringManager.ACTION_TETHER_STATE_CHANGED);
@@ -93,10 +127,9 @@
 
     @After
     public void tearDown() throws Exception {
+        mTM.stopAllTethering();
         mContext.unregisterReceiver(mTetherChangeReceiver);
-        InstrumentationRegistry.getInstrumentation()
-                .getUiAutomation()
-                .dropShellPermissionIdentity();
+        dropShellPermissionIdentity();
     }
 
     private class TetherChangeReceiver extends BroadcastReceiver {
@@ -125,28 +158,24 @@
 
         public final LinkedBlockingQueue<TetherState> mResult = new LinkedBlockingQueue<>();
 
-        // This method expects either an event where one of the interfaces is active, or events
-        // where the interfaces are available followed by one event where one of the interfaces is
-        // active. Here is a typical example for wifi tethering:
-        // AVAILABLE(wlan0) -> AVAILABLE(wlan1) -> ACTIVATE(wlan1).
-        public void expectActiveTethering(String[] ifaceRegexs) {
-            TetherState state = null;
+        // Expects that tethering reaches the desired state.
+        // - If active is true, expects that tethering is enabled on at least one interface
+        //   matching ifaceRegexs.
+        // - If active is false, expects that tethering is disabled on all the interfaces matching
+        //   ifaceRegexs.
+        // Fails if any interface matching ifaceRegexs becomes errored.
+        public void expectTethering(final boolean active, final String[] ifaceRegexs) {
             while (true) {
-                state = pollAndAssertNoError(DEFAULT_TIMEOUT_MS);
-                if (state == null) fail("Do not receive active state change broadcast");
+                final TetherState state = pollAndAssertNoError(DEFAULT_TIMEOUT_MS, ifaceRegexs);
+                assertNotNull("Did not receive expected state change, active: " + active, state);
 
-                if (isIfaceActive(ifaceRegexs, state)) return;
-
-                if (!isIfaceAvailable(ifaceRegexs, state)) break;
+                if (isIfaceActive(ifaceRegexs, state) == active) return;
             }
-
-            fail("Tethering is not actived, available ifaces: " + state.mAvailable.toString()
-                    + ", active ifaces: " + state.mActive.toString());
         }
 
-        private TetherState pollAndAssertNoError(final int timeout) {
+        private TetherState pollAndAssertNoError(final int timeout, final String[] ifaceRegexs) {
             final TetherState state = pollTetherState(timeout);
-            assertNoErroredIfaces(state);
+            assertNoErroredIfaces(state, ifaceRegexs);
             return state;
         }
 
@@ -163,54 +192,63 @@
             return isIfaceMatch(ifaceRegexs, state.mActive);
         }
 
-        private boolean isIfaceAvailable(final String[] ifaceRegexs, final TetherState state) {
-            return isIfaceMatch(ifaceRegexs, state.mAvailable);
-        }
-
-        // This method requires a broadcast to have been recorded iff the timeout is non-zero.
-        public void expectNoActiveTethering(final int timeout) {
-            final TetherState state = pollAndAssertNoError(timeout);
-
-            if (state == null) {
-                if (timeout != 0) {
-                    fail("Do not receive tethering state change broadcast");
-                }
-                return;
-            }
-
-            assertNoActiveIfaces(state);
-
-            for (final TetherState ts : mResult) {
-                assertNoErroredIfaces(ts);
-
-                assertNoActiveIfaces(ts);
-            }
-        }
-
-        private void assertNoErroredIfaces(final TetherState state) {
+        private void assertNoErroredIfaces(final TetherState state, final String[] ifaceRegexs) {
             if (state == null || state.mErrored == null) return;
 
-            if (state.mErrored.size() > 0) {
+            if (isIfaceMatch(ifaceRegexs, state.mErrored)) {
                 fail("Found failed tethering interfaces: " + Arrays.toString(state.mErrored.toArray()));
             }
         }
-
-        private void assertNoActiveIfaces(final TetherState state) {
-            if (state.mActive != null && state.mActive.size() > 0) {
-                fail("Found active tethering interface: " + Arrays.toString(state.mActive.toArray()));
-            }
-        }
     }
 
-    private class StartTetheringCallback implements TetheringManager.StartTetheringCallback {
+    private static class StartTetheringCallback implements TetheringManager.StartTetheringCallback {
+        private static int TIMEOUT_MS = 30_000;
+        public static class CallbackValue {
+            public final int error;
+
+            private CallbackValue(final int e) {
+                error = e;
+            }
+
+            public static class OnTetheringStarted extends CallbackValue {
+                OnTetheringStarted() { super(TETHER_ERROR_NO_ERROR); }
+            }
+
+            public static class OnTetheringFailed extends CallbackValue {
+                OnTetheringFailed(final int error) { super(error); }
+            }
+
+            @Override
+            public String toString() {
+                return String.format("%s(%d)", getClass().getSimpleName(), error);
+            }
+        }
+
+        private final ArrayTrackRecord<CallbackValue>.ReadHead mHistory =
+                new ArrayTrackRecord<CallbackValue>().newReadHead();
+
         @Override
         public void onTetheringStarted() {
-            // Do nothing, TetherChangeReceiver will wait until it receives the broadcast.
+            mHistory.add(new CallbackValue.OnTetheringStarted());
         }
 
         @Override
         public void onTetheringFailed(final int error) {
-            fail("startTethering fail: " + error);
+            mHistory.add(new CallbackValue.OnTetheringFailed(error));
+        }
+
+        public void verifyTetheringStarted() {
+            final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+            assertNotNull("No onTetheringStarted after " + TIMEOUT_MS + " ms", cv);
+            assertTrue("Fail start tethering:" + cv,
+                    cv instanceof CallbackValue.OnTetheringStarted);
+        }
+
+        public void expectTetheringFailed(final int expected) throws InterruptedException {
+            final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+            assertNotNull("No onTetheringFailed after " + TIMEOUT_MS + " ms", cv);
+            assertTrue("Expect fail with error code " + expected + ", but received: " + cv,
+                (cv instanceof CallbackValue.OnTetheringFailed) && (cv.error == expected));
         }
     }
 
@@ -241,15 +279,18 @@
         final String[] wifiRegexs = mTM.getTetherableWifiRegexs();
         if (wifiRegexs.length == 0) return;
 
-        mTetherChangeReceiver.expectNoActiveTethering(0 /** timeout */);
+        final String[] tetheredIfaces = mTM.getTetheredIfaces();
+        assertTrue(tetheredIfaces.length == 0);
 
         final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
-        mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(), c -> c.run(),
-                startTetheringCallback);
-        mTetherChangeReceiver.expectActiveTethering(wifiRegexs);
+        mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(),
+                c -> c.run() /* executor */, startTetheringCallback);
+        startTetheringCallback.verifyTetheringStarted();
+
+        mTetherChangeReceiver.expectTethering(true /* active */, wifiRegexs);
 
         mTM.stopTethering(TETHERING_WIFI);
-        mTetherChangeReceiver.expectNoActiveTethering(DEFAULT_TIMEOUT_MS);
+        mTetherChangeReceiver.expectTethering(false /* active */, wifiRegexs);
     }
 
     @Test
@@ -277,6 +318,8 @@
 
     // Must poll the callback before looking at the member.
     private static class TestTetheringEventCallback implements TetheringEventCallback {
+        private static final int TIMEOUT_MS = 30_000;
+
         public enum CallbackType {
             ON_SUPPORTED,
             ON_UPSTREAM,
@@ -299,7 +342,12 @@
                 this.callbackParam2 = param2;
             }
         }
-        private final LinkedBlockingQueue<CallbackValue> mCallbacks = new LinkedBlockingQueue<>();
+
+        private final ArrayTrackRecord<CallbackValue> mHistory =
+                new ArrayTrackRecord<CallbackValue>();
+
+        private final ArrayTrackRecord<CallbackValue>.ReadHead mCurrent =
+                mHistory.newReadHead();
 
         private TetheringInterfaceRegexps mTetherableRegex;
         private List<String> mTetherableIfaces;
@@ -307,108 +355,133 @@
 
         @Override
         public void onTetheringSupported(boolean supported) {
-            mCallbacks.add(new CallbackValue(CallbackType.ON_SUPPORTED, null, 0));
+            mHistory.add(new CallbackValue(CallbackType.ON_SUPPORTED, null, (supported ? 1 : 0)));
         }
 
         @Override
         public void onUpstreamChanged(Network network) {
-            mCallbacks.add(new CallbackValue(CallbackType.ON_UPSTREAM, network, 0));
+            mHistory.add(new CallbackValue(CallbackType.ON_UPSTREAM, network, 0));
         }
 
         @Override
         public void onTetherableInterfaceRegexpsChanged(TetheringInterfaceRegexps reg) {
             mTetherableRegex = reg;
-            mCallbacks.add(new CallbackValue(CallbackType.ON_TETHERABLE_REGEX, reg, 0));
+            mHistory.add(new CallbackValue(CallbackType.ON_TETHERABLE_REGEX, reg, 0));
         }
 
         @Override
         public void onTetherableInterfacesChanged(List<String> interfaces) {
             mTetherableIfaces = interfaces;
-            mCallbacks.add(new CallbackValue(CallbackType.ON_TETHERABLE_IFACES, interfaces, 0));
+            mHistory.add(new CallbackValue(CallbackType.ON_TETHERABLE_IFACES, interfaces, 0));
         }
 
         @Override
         public void onTetheredInterfacesChanged(List<String> interfaces) {
             mTetheredIfaces = interfaces;
-            mCallbacks.add(new CallbackValue(CallbackType.ON_TETHERED_IFACES, interfaces, 0));
+            mHistory.add(new CallbackValue(CallbackType.ON_TETHERED_IFACES, interfaces, 0));
         }
 
         @Override
         public void onError(String ifName, int error) {
-            mCallbacks.add(new CallbackValue(CallbackType.ON_ERROR, ifName, error));
+            mHistory.add(new CallbackValue(CallbackType.ON_ERROR, ifName, error));
         }
 
         @Override
         public void onClientsChanged(Collection<TetheredClient> clients) {
-            mCallbacks.add(new CallbackValue(CallbackType.ON_CLIENTS, clients, 0));
+            mHistory.add(new CallbackValue(CallbackType.ON_CLIENTS, clients, 0));
         }
 
         @Override
         public void onOffloadStatusChanged(int status) {
-            mCallbacks.add(new CallbackValue(CallbackType.ON_OFFLOAD_STATUS, status, 0));
-        }
-
-        public CallbackValue pollCallback() {
-            try {
-                return mCallbacks.poll(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-            } catch (InterruptedException e) {
-                fail("Callback not seen");
-            }
-            return null;
+            mHistory.add(new CallbackValue(CallbackType.ON_OFFLOAD_STATUS, status, 0));
         }
 
         public void expectTetherableInterfacesChanged(@NonNull List<String> regexs) {
-            while (true) {
-                final CallbackValue cv = pollCallback();
-                if (cv == null) fail("No expected tetherable ifaces callback");
-                if (cv.callbackType != CallbackType.ON_TETHERABLE_IFACES) continue;
-
-                final List<String> interfaces = (List<String>) cv.callbackParam;
-                if (isIfaceMatch(regexs, interfaces)) break;
-            }
+            assertNotNull("No expected tetherable ifaces callback", mCurrent.poll(TIMEOUT_MS,
+                (cv) -> {
+                    if (cv.callbackType != CallbackType.ON_TETHERABLE_IFACES) return false;
+                    final List<String> interfaces = (List<String>) cv.callbackParam;
+                    return isIfaceMatch(regexs, interfaces);
+                }));
         }
 
         public void expectTetheredInterfacesChanged(@NonNull List<String> regexs) {
-            while (true) {
-                final CallbackValue cv = pollCallback();
-                if (cv == null) fail("No expected tethered ifaces callback");
-                if (cv.callbackType != CallbackType.ON_TETHERED_IFACES) continue;
+            assertNotNull("No expected tethered ifaces callback", mCurrent.poll(TIMEOUT_MS,
+                (cv) -> {
+                    if (cv.callbackType != CallbackType.ON_TETHERED_IFACES) return false;
 
-                final List<String> interfaces = (List<String>) cv.callbackParam;
+                    final List<String> interfaces = (List<String>) cv.callbackParam;
 
-                // Null regexs means no active tethering.
-                if (regexs == null) {
-                    if (interfaces.size() == 0) break;
-                } else if (isIfaceMatch(regexs, interfaces)) {
-                    break;
-                }
-            }
+                    // Null regexs means no active tethering.
+                    if (regexs == null) return interfaces.isEmpty();
+
+                    return isIfaceMatch(regexs, interfaces);
+                }));
         }
 
         public void expectCallbackStarted() {
+            int receivedBitMap = 0;
             // The each bit represent a type from CallbackType.ON_*.
             // Expect all of callbacks except for ON_ERROR.
-            final int expectedBitMap = 0x7f ^ (1 << CallbackType.ON_ERROR.ordinal());
-            int receivedBitMap = 0;
-            while (receivedBitMap != expectedBitMap) {
-                final CallbackValue cv = pollCallback();
+            final int expectedBitMap = 0xff ^ (1 << CallbackType.ON_ERROR.ordinal());
+            // Receive ON_ERROR on started callback is not matter. It just means tethering is
+            // failed last time, should able to continue the test this time.
+            while ((receivedBitMap & expectedBitMap) != expectedBitMap) {
+                final CallbackValue cv = mCurrent.poll(TIMEOUT_MS, c -> true);
                 if (cv == null) {
                     fail("No expected callbacks, " + "expected bitmap: "
                             + expectedBitMap + ", actual: " + receivedBitMap);
                 }
-                receivedBitMap = receivedBitMap | (1 << cv.callbackType.ordinal());
+
+                receivedBitMap |= (1 << cv.callbackType.ordinal());
             }
         }
 
         public void expectOneOfOffloadStatusChanged(int... offloadStatuses) {
-            while (true) {
-                final CallbackValue cv = pollCallback();
-                if (cv == null) fail("No expected offload status change callback");
-                if (cv.callbackType != CallbackType.ON_OFFLOAD_STATUS) continue;
+            assertNotNull("No offload status changed", mCurrent.poll(TIMEOUT_MS, (cv) -> {
+                if (cv.callbackType != CallbackType.ON_OFFLOAD_STATUS) return false;
 
                 final int status = (int) cv.callbackParam;
-                for (int offloadStatus : offloadStatuses) if (offloadStatus == status) return;
-            }
+                for (int offloadStatus : offloadStatuses) if (offloadStatus == status) return true;
+
+                return false;
+            }));
+        }
+
+        public void expectErrorOrTethered(final String iface) {
+            assertNotNull("No expected callback", mCurrent.poll(TIMEOUT_MS, (cv) -> {
+                if (cv.callbackType == CallbackType.ON_ERROR
+                        && iface.equals((String) cv.callbackParam)) {
+                    return true;
+                }
+                if (cv.callbackType == CallbackType.ON_TETHERED_IFACES
+                        && ((List<String>) cv.callbackParam).contains(iface)) {
+                    return true;
+                }
+
+                return false;
+            }));
+        }
+
+        public Network getCurrentValidUpstream() {
+            final CallbackValue result = mCurrent.poll(TIMEOUT_MS, (cv) -> {
+                return (cv.callbackType == CallbackType.ON_UPSTREAM)
+                        && cv.callbackParam != null;
+            });
+
+            assertNotNull("No valid upstream", result);
+            return (Network) result.callbackParam;
+        }
+
+        public void assumeTetheringSupported() {
+            final ArrayTrackRecord<CallbackValue>.ReadHead history =
+                    mHistory.newReadHead();
+            assertNotNull("No onSupported callback", history.poll(TIMEOUT_MS, (cv) -> {
+                if (cv.callbackType != CallbackType.ON_SUPPORTED) return false;
+
+                assumeTrue(cv.callbackParam2 == 1 /* supported */);
+                return true;
+            }));
         }
 
         public TetheringInterfaceRegexps getTetheringInterfaceRegexps() {
@@ -424,52 +497,90 @@
         }
     }
 
-    @Test
-    public void testRegisterTetheringEventCallback() throws Exception {
-        if (!mTM.isTetheringSupported()) return;
+    private TestTetheringEventCallback registerTetheringEventCallback() {
+        final TestTetheringEventCallback tetherEventCallback =
+                new TestTetheringEventCallback();
 
-        final TestTetheringEventCallback tetherEventCallback = new TestTetheringEventCallback();
-
-        mTM.registerTetheringEventCallback(c -> c.run(), tetherEventCallback);
+        mTM.registerTetheringEventCallback(c -> c.run() /* executor */, tetherEventCallback);
         tetherEventCallback.expectCallbackStarted();
-        tetherEventCallback.expectOneOfOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
 
-        final TetheringInterfaceRegexps tetherableRegexs =
-                tetherEventCallback.getTetheringInterfaceRegexps();
-        final List<String> wifiRegexs = tetherableRegexs.getTetherableWifiRegexs();
-        if (wifiRegexs.size() == 0) return;
+        return tetherEventCallback;
+    }
 
-        final boolean isIfaceAvailWhenNoTethering =
-                isIfaceMatch(wifiRegexs, tetherEventCallback.getTetherableInterfaces());
+    private void unregisterTetheringEventCallback(final TestTetheringEventCallback callback) {
+        mTM.unregisterTetheringEventCallback(callback);
+    }
 
-        mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(), c -> c.run(),
-                new StartTetheringCallback());
+    private List<String> getWifiTetherableInterfaceRegexps(
+            final TestTetheringEventCallback callback) {
+        return callback.getTetheringInterfaceRegexps().getTetherableWifiRegexs();
+    }
 
-        // If interface is already available before starting tethering, the available callback may
-        // not be sent after tethering enabled.
-        if (!isIfaceAvailWhenNoTethering) {
-            tetherEventCallback.expectTetherableInterfacesChanged(wifiRegexs);
-        }
+    private boolean isWifiTetheringSupported(final TestTetheringEventCallback callback) {
+        return !getWifiTetherableInterfaceRegexps(callback).isEmpty();
+    }
 
-        tetherEventCallback.expectTetheredInterfacesChanged(wifiRegexs);
-        tetherEventCallback.expectOneOfOffloadStatusChanged(
+    private void startWifiTethering(final TestTetheringEventCallback callback)
+            throws InterruptedException {
+        final List<String> wifiRegexs = getWifiTetherableInterfaceRegexps(callback);
+        assertFalse(isIfaceMatch(wifiRegexs, callback.getTetheredInterfaces()));
+
+        final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
+        mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(),
+                c -> c.run() /* executor */, startTetheringCallback);
+        startTetheringCallback.verifyTetheringStarted();
+
+        callback.expectTetheredInterfacesChanged(wifiRegexs);
+
+        callback.expectOneOfOffloadStatusChanged(
                 TETHER_HARDWARE_OFFLOAD_STARTED,
                 TETHER_HARDWARE_OFFLOAD_FAILED);
+    }
 
+    private void stopWifiTethering(final TestTetheringEventCallback callback) {
         mTM.stopTethering(TETHERING_WIFI);
+        callback.expectTetheredInterfacesChanged(null);
+        callback.expectOneOfOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
+    }
 
-        tetherEventCallback.expectTetheredInterfacesChanged(null);
-        tetherEventCallback.expectOneOfOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
-        mTM.unregisterTetheringEventCallback(tetherEventCallback);
+    @Test
+    public void testRegisterTetheringEventCallback() throws Exception {
+        final TestTetheringEventCallback tetherEventCallback = registerTetheringEventCallback();
+        tetherEventCallback.assumeTetheringSupported();
+
+        if (!isWifiTetheringSupported(tetherEventCallback)) {
+            unregisterTetheringEventCallback(tetherEventCallback);
+            return;
+        }
+
+        startWifiTethering(tetherEventCallback);
+
+        final List<String> tetheredIfaces = tetherEventCallback.getTetheredInterfaces();
+        assertEquals(1, tetheredIfaces.size());
+        final String wifiTetheringIface = tetheredIfaces.get(0);
+
+        stopWifiTethering(tetherEventCallback);
+
+        try {
+            final int ret = mTM.tether(wifiTetheringIface);
+
+            // There is no guarantee that the wifi interface will be available after disabling
+            // the hotspot, so don't fail the test if the call to tether() fails.
+            assumeTrue(ret == TETHER_ERROR_NO_ERROR);
+
+            // If calling #tether successful, there is a callback to tell the result of tethering
+            // setup.
+            tetherEventCallback.expectErrorOrTethered(wifiTetheringIface);
+        } finally {
+            mTM.untether(wifiTetheringIface);
+            unregisterTetheringEventCallback(tetherEventCallback);
+        }
     }
 
     @Test
     public void testGetTetherableInterfaceRegexps() {
-        if (!mTM.isTetheringSupported()) return;
-
-        final TestTetheringEventCallback tetherEventCallback = new TestTetheringEventCallback();
-        mTM.registerTetheringEventCallback(c -> c.run(), tetherEventCallback);
-        tetherEventCallback.expectCallbackStarted();
+        final TestTetheringEventCallback tetherEventCallback = registerTetheringEventCallback();
+        tetherEventCallback.assumeTetheringSupported();
 
         final TetheringInterfaceRegexps tetherableRegexs =
                 tetherEventCallback.getTetheringInterfaceRegexps();
@@ -486,7 +597,36 @@
         wifiRegexs.forEach(s -> assertFalse(btRegexs.contains(s)));
         usbRegexs.forEach(s -> assertFalse(btRegexs.contains(s)));
 
-        mTM.unregisterTetheringEventCallback(tetherEventCallback);
+        unregisterTetheringEventCallback(tetherEventCallback);
+    }
+
+    @Test
+    public void testStopAllTethering() throws Exception {
+        final TestTetheringEventCallback tetherEventCallback = registerTetheringEventCallback();
+        tetherEventCallback.assumeTetheringSupported();
+
+        try {
+            if (!isWifiTetheringSupported(tetherEventCallback)) return;
+
+            // TODO: start ethernet tethering here when TetheringManagerTest is moved to
+            // TetheringIntegrationTest.
+
+            startWifiTethering(tetherEventCallback);
+
+            mTM.stopAllTethering();
+            tetherEventCallback.expectTetheredInterfacesChanged(null);
+        } finally {
+            unregisterTetheringEventCallback(tetherEventCallback);
+        }
+    }
+
+    @Test
+    public void testEnableTetheringPermission() throws Exception {
+        dropShellPermissionIdentity();
+        final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
+        mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(),
+                c -> c.run() /* executor */, startTetheringCallback);
+        startTetheringCallback.expectTetheringFailed(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
     }
 
     private class EntitlementResultListener implements OnTetheringEntitlementResultListener {
@@ -537,4 +677,48 @@
                     TETHERING_WIFI, false, c -> c.run(), null);
         } catch (IllegalArgumentException expect) { }
     }
+
+    @Test
+    public void testTetheringUpstream() throws Exception {
+        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
+        final TestTetheringEventCallback tetherEventCallback = registerTetheringEventCallback();
+        tetherEventCallback.assumeTetheringSupported();
+        final boolean previousWifiEnabledState = mWm.isWifiEnabled();
+
+        try {
+            if (!isWifiTetheringSupported(tetherEventCallback)) return;
+
+            if (previousWifiEnabledState) {
+                mCtsNetUtils.disconnectFromWifi(null);
+            }
+
+            final Network activeNetwork = mCm.getActiveNetwork();
+            assertNotNull("No active network. Please ensure the device has working mobile data.",
+                    activeNetwork);
+            final NetworkCapabilities activeNetCap = mCm.getNetworkCapabilities(activeNetwork);
+
+            // If active nework is ETHERNET, tethering may not use cell network as upstream.
+            assumeFalse(activeNetCap.hasTransport(TRANSPORT_ETHERNET));
+
+            assertTrue(activeNetCap.hasTransport(TRANSPORT_CELLULAR));
+
+            startWifiTethering(tetherEventCallback);
+
+            final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(
+                    Context.TELEPHONY_SERVICE);
+            final boolean dunRequired = telephonyManager.isTetheringApnRequired();
+            final int expectedCap = dunRequired ? NET_CAPABILITY_DUN : NET_CAPABILITY_INTERNET;
+            final Network network = tetherEventCallback.getCurrentValidUpstream();
+            final NetworkCapabilities netCap = mCm.getNetworkCapabilities(network);
+            assertTrue(netCap.hasTransport(TRANSPORT_CELLULAR));
+            assertTrue(netCap.hasCapability(expectedCap));
+
+            stopWifiTethering(tetherEventCallback);
+        } finally {
+            unregisterTetheringEventCallback(tetherEventCallback);
+            if (previousWifiEnabledState) {
+                mCtsNetUtils.connectToWifi();
+            }
+        }
+    }
 }