/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.net.cts;

import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.Manifest.permission.ACCESS_NETWORK_STATE;
import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
import static android.Manifest.permission.NETWORK_FACTORY;
import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.Manifest.permission.NETWORK_SETUP_WIZARD;
import static android.Manifest.permission.NETWORK_STACK;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.content.pm.PackageManager.FEATURE_BLUETOOTH;
import static android.content.pm.PackageManager.FEATURE_ETHERNET;
import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
import static android.content.pm.PackageManager.FEATURE_USB_HOST;
import static android.content.pm.PackageManager.FEATURE_WATCH;
import static android.content.pm.PackageManager.FEATURE_WIFI;
import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.net.ConnectivityManager.EXTRA_NETWORK;
import static android.net.ConnectivityManager.EXTRA_NETWORK_REQUEST;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
import static android.net.ConnectivityManager.TYPE_ETHERNET;
import static android.net.ConnectivityManager.TYPE_MOBILE_CBS;
import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
import static android.net.ConnectivityManager.TYPE_MOBILE_EMERGENCY;
import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA;
import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
import static android.net.ConnectivityManager.TYPE_MOBILE_IA;
import static android.net.ConnectivityManager.TYPE_MOBILE_IMS;
import static android.net.ConnectivityManager.TYPE_MOBILE_MMS;
import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL;
import static android.net.ConnectivityManager.TYPE_PROXY;
import static android.net.ConnectivityManager.TYPE_VPN;
import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
import static android.net.ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
import static android.net.cts.util.CtsNetUtils.ConnectivityActionReceiver;
import static android.net.cts.util.CtsNetUtils.HTTP_PORT;
import static android.net.cts.util.CtsNetUtils.NETWORK_CALLBACK_ACTION;
import static android.net.cts.util.CtsNetUtils.TEST_HOST;
import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
import static android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
import static android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL;
import static android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL;
import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
import static android.os.Process.INVALID_UID;
import static android.provider.Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE;
import static android.system.OsConstants.AF_INET;
import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.AF_UNSPEC;

import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_LOCKDOWN_VPN;
import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE;
import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.MiscAsserts.assertThrows;
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;

import android.annotation.NonNull;
import android.app.Instrumentation;
import android.app.PendingIntent;
import android.app.UiAutomation;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.CaptivePortalData;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.ConnectivitySettingsManager;
import android.net.InetAddresses;
import android.net.IpSecManager;
import android.net.IpSecManager.UdpEncapsulationSocket;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkAgent;
import android.net.NetworkAgentConfig;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkInfo.DetailedState;
import android.net.NetworkInfo.State;
import android.net.NetworkProvider;
import android.net.NetworkRequest;
import android.net.NetworkSpecifier;
import android.net.NetworkStateSnapshot;
import android.net.OemNetworkPreferences;
import android.net.ProxyInfo;
import android.net.SocketKeepalive;
import android.net.TelephonyNetworkSpecifier;
import android.net.TestNetworkInterface;
import android.net.TestNetworkManager;
import android.net.Uri;
import android.net.cts.util.CtsNetUtils;
import android.net.cts.util.CtsTetheringUtils;
import android.net.util.KeepaliveUtils;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.MessageQueue;
import android.os.Process;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.VintfRuntimeInfo;
import android.platform.test.annotations.AppModeFull;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.Range;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.RequiresDevice;
import androidx.test.runner.AndroidJUnit4;

import com.android.internal.util.ArrayUtils;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
import com.android.networkstack.apishim.ConstantsShim;
import com.android.networkstack.apishim.NetworkInformationShimImpl;
import com.android.networkstack.apishim.common.ConnectivityManagerShim;
import com.android.testutils.CompatUtil;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRuleKt;
import com.android.testutils.DeviceInfoUtils;
import com.android.testutils.DumpTestUtils;
import com.android.testutils.RecorderCallback.CallbackEntry;
import com.android.testutils.TestHttpServer;
import com.android.testutils.TestNetworkTracker;
import com.android.testutils.TestableNetworkCallback;

import junit.framework.AssertionFailedError;

import libcore.io.Streams;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.HttpURLConnection;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import fi.iki.elonen.NanoHTTPD.Method;
import fi.iki.elonen.NanoHTTPD.Response.IStatus;
import fi.iki.elonen.NanoHTTPD.Response.Status;

@RunWith(AndroidJUnit4.class)
public class ConnectivityManagerTest {
    @Rule
    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();

    @Rule
    public final DeviceConfigRule mTestValidationConfigRule = new DeviceConfigRule(
            5 /* retryCountBeforeSIfConfigChanged */);

    private static final String TAG = ConnectivityManagerTest.class.getSimpleName();

    public static final int TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE;
    public static final int TYPE_WIFI = ConnectivityManager.TYPE_WIFI;

    private static final int HOST_ADDRESS = 0x7f000001;// represent ip 127.0.0.1
    private static final int KEEPALIVE_CALLBACK_TIMEOUT_MS = 2000;
    private static final int INTERVAL_KEEPALIVE_RETRY_MS = 500;
    private static final int MAX_KEEPALIVE_RETRY_COUNT = 3;
    private static final int MIN_KEEPALIVE_INTERVAL = 10;

    private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
    private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 5_000;
    private static final int NO_CALLBACK_TIMEOUT_MS = 100;
    private static final int SOCKET_TIMEOUT_MS = 100;
    private static final int NUM_TRIES_MULTIPATH_PREF_CHECK = 20;
    private static final long INTERVAL_MULTIPATH_PREF_CHECK_MS = 500;
    // device could have only one interface: data, wifi.
    private static final int MIN_NUM_NETWORK_TYPES = 1;

    // Airplane Mode BroadcastReceiver Timeout
    private static final long AIRPLANE_MODE_CHANGE_TIMEOUT_MS = 10_000L;

    // Timeout for applying uids allowed on restricted networks
    private static final long APPLYING_UIDS_ALLOWED_ON_RESTRICTED_NETWORKS_TIMEOUT_MS = 3_000L;

    // Minimum supported keepalive counts for wifi and cellular.
    public static final int MIN_SUPPORTED_CELLULAR_KEEPALIVE_COUNT = 1;
    public static final int MIN_SUPPORTED_WIFI_KEEPALIVE_COUNT = 3;

    private static final String NETWORK_METERED_MULTIPATH_PREFERENCE_RES_NAME =
            "config_networkMeteredMultipathPreference";
    private static final String KEEPALIVE_ALLOWED_UNPRIVILEGED_RES_NAME =
            "config_allowedUnprivilegedKeepalivePerUid";
    private static final String KEEPALIVE_RESERVED_PER_SLOT_RES_NAME =
            "config_reservedPrivilegedKeepaliveSlots";
    private static final String TEST_RESTRICTED_NW_IFACE_NAME = "test-restricted-nw";

    private static final LinkAddress TEST_LINKADDR = new LinkAddress(
            InetAddresses.parseNumericAddress("2001:db8::8"), 64);

    private static final int AIRPLANE_MODE_OFF = 0;
    private static final int AIRPLANE_MODE_ON = 1;

    private static final String TEST_HTTPS_URL_PATH = "/https_path";
    private static final String TEST_HTTP_URL_PATH = "/http_path";
    private static final String LOCALHOST_HOSTNAME = "localhost";
    // Re-connecting to the AP, obtaining an IP address, revalidating can take a long time
    private static final long WIFI_CONNECT_TIMEOUT_MS = 60_000L;

    private Context mContext;
    private Instrumentation mInstrumentation;
    private ConnectivityManager mCm;
    private ConnectivityManagerShim mCmShim;
    private WifiManager mWifiManager;
    private PackageManager mPackageManager;
    private TelephonyManager mTm;
    private final ArraySet<Integer> mNetworkTypes = new ArraySet<>();
    private UiAutomation mUiAutomation;
    private CtsNetUtils mCtsNetUtils;
    // The registered callbacks.
    private List<NetworkCallback> mRegisteredCallbacks = new ArrayList<>();
    // Used for cleanup purposes.
    private final List<Range<Integer>> mVpnRequiredUidRanges = new ArrayList<>();

    private final TestHttpServer mHttpServer = new TestHttpServer(LOCALHOST_HOSTNAME);

    @Before
    public void setUp() throws Exception {
        mInstrumentation = InstrumentationRegistry.getInstrumentation();
        mContext = mInstrumentation.getContext();
        mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
        mCmShim = ConnectivityManagerShimImpl.newInstance(mContext);
        mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
        mPackageManager = mContext.getPackageManager();
        mCtsNetUtils = new CtsNetUtils(mContext);
        mTm = mContext.getSystemService(TelephonyManager.class);

        if (DevSdkIgnoreRuleKt.isDevSdkInRange(null /* minExclusive */,
                Build.VERSION_CODES.R /* maxInclusive */)) {
            addLegacySupportedNetworkTypes();
        } else {
            addSupportedNetworkTypes();
        }

        mUiAutomation = mInstrumentation.getUiAutomation();

        assertNotNull("CTS requires a working Internet connection", mCm.getActiveNetwork());
    }

    private void addLegacySupportedNetworkTypes() {
        // Network type support as expected for android R-
        // Get com.android.internal.R.array.networkAttributes
        int resId = mContext.getResources().getIdentifier("networkAttributes", "array", "android");
        String[] naStrings = mContext.getResources().getStringArray(resId);
        boolean wifiOnly = SystemProperties.getBoolean("ro.radio.noril", false);
        for (String naString : naStrings) {
            try {
                final String[] splitConfig = naString.split(",");
                // Format was name,type,radio,priority,restoreTime,dependencyMet
                final int type = Integer.parseInt(splitConfig[1]);
                if (wifiOnly && ConnectivityManager.isNetworkTypeMobile(type)) {
                    continue;
                }
                mNetworkTypes.add(type);
            } catch (Exception e) {}
        }
    }

    private void addSupportedNetworkTypes() {
        final PackageManager pm = mContext.getPackageManager();
        if (pm.hasSystemFeature(FEATURE_WIFI)) {
            mNetworkTypes.add(TYPE_WIFI);
        }
        if (pm.hasSystemFeature(FEATURE_WIFI_DIRECT)) {
            mNetworkTypes.add(TYPE_WIFI_P2P);
        }
        if (mContext.getSystemService(TelephonyManager.class).isDataCapable()) {
            mNetworkTypes.add(TYPE_MOBILE);
            mNetworkTypes.add(TYPE_MOBILE_MMS);
            mNetworkTypes.add(TYPE_MOBILE_SUPL);
            mNetworkTypes.add(TYPE_MOBILE_DUN);
            mNetworkTypes.add(TYPE_MOBILE_HIPRI);
            mNetworkTypes.add(TYPE_MOBILE_FOTA);
            mNetworkTypes.add(TYPE_MOBILE_IMS);
            mNetworkTypes.add(TYPE_MOBILE_CBS);
            mNetworkTypes.add(TYPE_MOBILE_IA);
            mNetworkTypes.add(TYPE_MOBILE_EMERGENCY);
        }
        if (pm.hasSystemFeature(FEATURE_BLUETOOTH)) {
            mNetworkTypes.add(TYPE_BLUETOOTH);
        }
        if (pm.hasSystemFeature(FEATURE_WATCH)) {
            mNetworkTypes.add(TYPE_PROXY);
        }
        if (mContext.getSystemService(Context.ETHERNET_SERVICE) != null) {
            mNetworkTypes.add(TYPE_ETHERNET);
        }
        mNetworkTypes.add(TYPE_VPN);
    }

    @After
    public void tearDown() throws Exception {
        // Release any NetworkRequests filed to connect mobile data.
        if (mCtsNetUtils.cellConnectAttempted()) {
            mCtsNetUtils.disconnectFromCell();
        }

        if (TestUtils.shouldTestSApis()) {
            runWithShellPermissionIdentity(
                    () -> mCmShim.setRequireVpnForUids(false, mVpnRequiredUidRanges),
                    NETWORK_SETTINGS);
        }

        // All tests in this class require a working Internet connection as they start. Make
        // sure there is still one as they end that's ready to use for the next test to use.
        final TestNetworkCallback callback = new TestNetworkCallback();
        registerDefaultNetworkCallback(callback);
        try {
            assertNotNull("Couldn't restore Internet connectivity", callback.waitForAvailable());
        } finally {
            // Unregister all registered callbacks.
            unregisterRegisteredCallbacks();
        }
    }

    @Test
    public void testIsNetworkTypeValid() {
        assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE));
        assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_WIFI));
        assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_MMS));
        assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_SUPL));
        assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_DUN));
        assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_HIPRI));
        assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_WIMAX));
        assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_BLUETOOTH));
        assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_DUMMY));
        assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_ETHERNET));
        assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_FOTA));
        assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_IMS));
        assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_CBS));
        assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_WIFI_P2P));
        assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_IA));
        assertFalse(mCm.isNetworkTypeValid(-1));
        assertTrue(mCm.isNetworkTypeValid(0));
        assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.MAX_NETWORK_TYPE));
        assertFalse(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.MAX_NETWORK_TYPE+1));

        NetworkInfo[] ni = mCm.getAllNetworkInfo();

        for (NetworkInfo n: ni) {
            assertTrue(ConnectivityManager.isNetworkTypeValid(n.getType()));
        }

    }

    @Test
    public void testSetNetworkPreference() {
        // getNetworkPreference() and setNetworkPreference() are both deprecated so they do
        // not preform any action.  Verify they are at least still callable.
        mCm.setNetworkPreference(mCm.getNetworkPreference());
    }

    @Test
    public void testGetActiveNetworkInfo() {
        NetworkInfo ni = mCm.getActiveNetworkInfo();

        assertNotNull("You must have an active network connection to complete CTS", ni);
        assertTrue(ConnectivityManager.isNetworkTypeValid(ni.getType()));
        assertTrue(ni.getState() == State.CONNECTED);
    }

    @Test
    public void testGetActiveNetwork() {
        Network network = mCm.getActiveNetwork();
        assertNotNull("You must have an active network connection to complete CTS", network);

        NetworkInfo ni = mCm.getNetworkInfo(network);
        assertNotNull("Network returned from getActiveNetwork was invalid", ni);

        // Similar to testGetActiveNetworkInfo above.
        assertTrue(ConnectivityManager.isNetworkTypeValid(ni.getType()));
        assertTrue(ni.getState() == State.CONNECTED);
    }

    @Test
    public void testGetNetworkInfo() {
        for (int type = -1; type <= ConnectivityManager.MAX_NETWORK_TYPE+1; type++) {
            if (shouldBeSupported(type)) {
                NetworkInfo ni = mCm.getNetworkInfo(type);
                assertTrue("Info shouldn't be null for " + type, ni != null);
                State state = ni.getState();
                assertTrue("Bad state for " + type, State.UNKNOWN.ordinal() >= state.ordinal()
                           && state.ordinal() >= State.CONNECTING.ordinal());
                DetailedState ds = ni.getDetailedState();
                assertTrue("Bad detailed state for " + type,
                           DetailedState.FAILED.ordinal() >= ds.ordinal()
                           && ds.ordinal() >= DetailedState.IDLE.ordinal());
            } else {
                assertNull("Info should be null for " + type, mCm.getNetworkInfo(type));
            }
        }
    }

    @Test
    public void testGetAllNetworkInfo() {
        NetworkInfo[] ni = mCm.getAllNetworkInfo();
        assertTrue(ni.length >= MIN_NUM_NETWORK_TYPES);
        for (int type = 0; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) {
            int desiredFoundCount = (shouldBeSupported(type) ? 1 : 0);
            int foundCount = 0;
            for (NetworkInfo i : ni) {
                if (i.getType() == type) foundCount++;
            }
            if (foundCount != desiredFoundCount) {
                Log.e(TAG, "failure in testGetAllNetworkInfo.  Dump of returned NetworkInfos:");
                for (NetworkInfo networkInfo : ni) Log.e(TAG, "  " + networkInfo);
            }
            assertTrue("Unexpected foundCount of " + foundCount + " for type " + type,
                    foundCount == desiredFoundCount);
        }
    }

    private String getSubscriberIdForCellNetwork(Network cellNetwork) {
        final NetworkCapabilities cellCaps = mCm.getNetworkCapabilities(cellNetwork);
        final NetworkSpecifier specifier = cellCaps.getNetworkSpecifier();
        assertTrue(specifier instanceof TelephonyNetworkSpecifier);
        // Get subscription from Telephony network specifier.
        final int subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
        assertNotEquals(SubscriptionManager.INVALID_SUBSCRIPTION_ID, subId);

        // Get subscriber Id from telephony manager.
        final TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
        return runWithShellPermissionIdentity(() -> tm.getSubscriberId(subId),
                android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
    }

    @AppModeFull(reason = "Cannot request network in instant app mode")
    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
    @Test
    public void testGetAllNetworkStateSnapshots()
            throws InterruptedException {
        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
        // Make sure cell is active to retrieve IMSI for verification in later step.
        final Network cellNetwork = mCtsNetUtils.connectToCell();
        final String subscriberId = getSubscriberIdForCellNetwork(cellNetwork);
        assertFalse(TextUtils.isEmpty(subscriberId));

        // Verify the API cannot be called without proper permission.
        assertThrows(SecurityException.class, () -> mCm.getAllNetworkStateSnapshots());

        // Get all networks, verify the result of getAllNetworkStateSnapshots matches the result
        // got from other APIs.
        final Network[] networks = mCm.getAllNetworks();
        assertGreaterOrEqual(networks.length, 1);
        final List<NetworkStateSnapshot> snapshots = runWithShellPermissionIdentity(
                () -> mCm.getAllNetworkStateSnapshots(), NETWORK_SETTINGS);
        assertEquals(networks.length, snapshots.size());
        for (final Network network : networks) {
            // Can't use a lambda because it will cause the test to crash on R with
            // NoClassDefFoundError.
            NetworkStateSnapshot snapshot = null;
            for (NetworkStateSnapshot item : snapshots) {
                if (item.getNetwork().equals(network)) {
                    snapshot = item;
                    break;
                }
            }
            assertNotNull(snapshot);
            final NetworkCapabilities caps =
                    Objects.requireNonNull(mCm.getNetworkCapabilities(network));
            // Redact specifier of the capabilities of the snapshot before comparing since
            // the result returned from getNetworkCapabilities always get redacted.
            final NetworkSpecifier snapshotCapSpecifier =
                    snapshot.getNetworkCapabilities().getNetworkSpecifier();
            final NetworkSpecifier redactedSnapshotCapSpecifier =
                    snapshotCapSpecifier == null ? null : snapshotCapSpecifier.redact();
            assertEquals("", caps.describeImmutableDifferences(
                    snapshot.getNetworkCapabilities()
                            .setNetworkSpecifier(redactedSnapshotCapSpecifier)));
            assertEquals(mCm.getLinkProperties(network), snapshot.getLinkProperties());
            assertEquals(mCm.getNetworkInfo(network).getType(), snapshot.getLegacyType());

            if (network.equals(cellNetwork)) {
                assertEquals(subscriberId, snapshot.getSubscriberId());
            }
        }
    }

    private boolean checkPermission(String perm, int uid) {
        return mContext.checkPermission(perm, -1 /* pid */, uid) == PERMISSION_GRANTED;
    }

    private String findPackageByPermissions(@NonNull List<String> requiredPermissions,
                @NonNull List<String> forbiddenPermissions) throws Exception {
        final List<PackageInfo> packageInfos =
                mPackageManager.getInstalledPackages(GET_PERMISSIONS);
        for (PackageInfo packageInfo : packageInfos) {
            final int uid = mPackageManager.getPackageUid(packageInfo.packageName, 0 /* flags */);
            if (!CollectionUtils.all(requiredPermissions, perm -> checkPermission(perm, uid))) {
                continue;
            }
            if (CollectionUtils.any(forbiddenPermissions, perm -> checkPermission(perm, uid))) {
                continue;
            }

            return packageInfo.packageName;
        }
        return null;
    }

    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
    @AppModeFull(reason = "Cannot get installed packages in instant app mode")
    @Test
    public void testGetRedactedLinkPropertiesForPackage() throws Exception {
        final String groundedPkg = findPackageByPermissions(
                List.of(), /* requiredPermissions */
                List.of(ACCESS_NETWORK_STATE) /* forbiddenPermissions */);
        assertNotNull("Couldn't find any package without ACCESS_NETWORK_STATE", groundedPkg);
        final int groundedUid = mPackageManager.getPackageUid(groundedPkg, 0 /* flags */);

        final String normalPkg = findPackageByPermissions(
                List.of(ACCESS_NETWORK_STATE) /* requiredPermissions */,
                List.of(NETWORK_SETTINGS, NETWORK_STACK,
                        PERMISSION_MAINLINE_NETWORK_STACK) /* forbiddenPermissions */);
        assertNotNull("Couldn't find any package with ACCESS_NETWORK_STATE but"
                + " without NETWORK_SETTINGS", normalPkg);
        final int normalUid = mPackageManager.getPackageUid(normalPkg, 0 /* flags */);

        // There are some privileged packages on the system, like the phone process, the network
        // stack and the system server.
        final String privilegedPkg = findPackageByPermissions(
                List.of(ACCESS_NETWORK_STATE, NETWORK_SETTINGS), /* requiredPermissions */
                List.of() /* forbiddenPermissions */);
        assertNotNull("Couldn't find a package with sufficient permissions", privilegedPkg);
        final int privilegedUid = mPackageManager.getPackageUid(privilegedPkg, 0);

        // Set parcelSensitiveFields to true to preserve CaptivePortalApiUrl & CaptivePortalData
        // when parceling.
        final LinkProperties lp = new LinkProperties(new LinkProperties(),
                true /* parcelSensitiveFields */);
        final Uri capportUrl = Uri.parse("https://capport.example.com/api");
        final CaptivePortalData capportData = new CaptivePortalData.Builder().build();
        final int mtu = 12345;
        lp.setMtu(mtu);
        lp.setCaptivePortalApiUrl(capportUrl);
        lp.setCaptivePortalData(capportData);

        // No matter what the given uid is, a SecurityException will be thrown if the caller
        // doesn't hold the NETWORK_SETTINGS permission.
        assertThrows(SecurityException.class,
                () -> mCm.getRedactedLinkPropertiesForPackage(lp, groundedUid, groundedPkg));
        assertThrows(SecurityException.class,
                () -> mCm.getRedactedLinkPropertiesForPackage(lp, normalUid, normalPkg));
        assertThrows(SecurityException.class,
                () -> mCm.getRedactedLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg));

        runAsShell(NETWORK_SETTINGS, () -> {
            // No matter what the given uid is, if the given LinkProperties is null, then
            // NullPointerException will be thrown.
            assertThrows(NullPointerException.class,
                    () -> mCm.getRedactedLinkPropertiesForPackage(null, groundedUid, groundedPkg));
            assertThrows(NullPointerException.class,
                    () -> mCm.getRedactedLinkPropertiesForPackage(null, normalUid, normalPkg));
            assertThrows(NullPointerException.class,
                    () -> mCm.getRedactedLinkPropertiesForPackage(
                            null, privilegedUid, privilegedPkg));

            // Make sure null is returned for a UID without ACCESS_NETWORK_STATE.
            assertNull(mCm.getRedactedLinkPropertiesForPackage(lp, groundedUid, groundedPkg));

            // CaptivePortalApiUrl & CaptivePortalData will be set to null if given uid doesn't hold
            // the NETWORK_SETTINGS permission.
            assertNull(mCm.getRedactedLinkPropertiesForPackage(lp, normalUid, normalPkg)
                    .getCaptivePortalApiUrl());
            assertNull(mCm.getRedactedLinkPropertiesForPackage(lp, normalUid, normalPkg)
                    .getCaptivePortalData());
            // MTU is not sensitive and is not redacted.
            assertEquals(mtu, mCm.getRedactedLinkPropertiesForPackage(lp, normalUid, normalPkg)
                    .getMtu());

            // CaptivePortalApiUrl & CaptivePortalData will be preserved if the given uid holds the
            // NETWORK_SETTINGS permission.
            assertNotNull(lp.getCaptivePortalApiUrl());
            assertNotNull(lp.getCaptivePortalData());
            assertEquals(lp.getCaptivePortalApiUrl(),
                    mCm.getRedactedLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
                            .getCaptivePortalApiUrl());
            assertEquals(lp.getCaptivePortalData(),
                    mCm.getRedactedLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
                            .getCaptivePortalData());
        });
    }

    private NetworkCapabilities redactNc(@NonNull final NetworkCapabilities nc, int uid,
            @NonNull String packageName) {
        return mCm.getRedactedNetworkCapabilitiesForPackage(nc, uid, packageName);
    }

    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
    @AppModeFull(reason = "Cannot get installed packages in instant app mode")
    @Test
    public void testGetRedactedNetworkCapabilitiesForPackage() throws Exception {
        final String groundedPkg = findPackageByPermissions(
                List.of(), /* requiredPermissions */
                List.of(ACCESS_NETWORK_STATE) /* forbiddenPermissions */);
        assertNotNull("Couldn't find any package without ACCESS_NETWORK_STATE", groundedPkg);
        final int groundedUid = mPackageManager.getPackageUid(groundedPkg, 0 /* flags */);

        // A package which doesn't have any of the permissions below, but has NETWORK_STATE.
        // There should be a number of packages like this on the device; AOSP has many,
        // including contacts, webview, the keyboard, pacprocessor, messaging.
        final String normalPkg = findPackageByPermissions(
                List.of(ACCESS_NETWORK_STATE) /* requiredPermissions */,
                List.of(NETWORK_SETTINGS, NETWORK_FACTORY, NETWORK_SETUP_WIZARD,
                        NETWORK_STACK, PERMISSION_MAINLINE_NETWORK_STACK,
                        ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION) /* forbiddenPermissions */);
        assertNotNull("Can't find a package with ACCESS_NETWORK_STATE but without any of"
                + " the forbidden permissions", normalPkg);
        final int normalUid = mPackageManager.getPackageUid(normalPkg, 0 /* flags */);

        // There are some privileged packages on the system, like the phone process, the network
        // stack and the system server.
        final String privilegedPkg = findPackageByPermissions(
                List.of(ACCESS_NETWORK_STATE, NETWORK_SETTINGS, NETWORK_FACTORY,
                        ACCESS_FINE_LOCATION), /* requiredPermissions */
                List.of() /* forbiddenPermissions */);
        assertNotNull("Couldn't find a package with sufficient permissions", privilegedPkg);
        final int privilegedUid = mPackageManager.getPackageUid(privilegedPkg, 0);

        final Set<Range<Integer>> uids = new ArraySet<>();
        uids.add(new Range<>(10000, 10100));
        uids.add(new Range<>(10200, 10300));
        final String ssid = "My-WiFi";
        // This test will set underlying networks in the capabilities to redact to see if they
        // are appropriately redacted, so fetch the default network to put in there as an example.
        final Network defaultNetwork = mCm.getActiveNetwork();
        assertNotNull("CTS requires a working Internet connection", defaultNetwork);
        final int subId1 = 1;
        final int subId2 = 2;
        final int[] administratorUids = {normalUid};
        final String bssid = "location sensitive";
        final int rssi = 43; // not location sensitive
        final WifiInfo wifiInfo = new WifiInfo.Builder()
                .setBssid(bssid)
                .setRssi(rssi)
                .build();
        final NetworkCapabilities nc = new NetworkCapabilities.Builder()
                .setUids(uids)
                .setSsid(ssid)
                .setUnderlyingNetworks(List.of(defaultNetwork))
                .setSubscriptionIds(Set.of(subId1, subId2))
                .setAdministratorUids(administratorUids)
                .setOwnerUid(normalUid)
                .setTransportInfo(wifiInfo)
                .build();

        // No matter what the given uid is, a SecurityException will be thrown if the caller
        // doesn't hold the NETWORK_SETTINGS permission.
        assertThrows(SecurityException.class, () -> redactNc(nc, groundedUid, groundedPkg));
        assertThrows(SecurityException.class, () -> redactNc(nc, normalUid, normalPkg));
        assertThrows(SecurityException.class, () -> redactNc(nc, privilegedUid, privilegedPkg));

        runAsShell(NETWORK_SETTINGS, () -> {
            // Make sure that the NC is null if the package doesn't hold ACCESS_NETWORK_STATE.
            assertNull(redactNc(nc, groundedUid, groundedPkg));

            // Uids, ssid, underlying networks & subscriptionIds will be redacted if the given uid
            // doesn't hold the associated permissions. The wifi transport info is also suitably
            // redacted.
            final NetworkCapabilities redactedNormal = redactNc(nc, normalUid, normalPkg);
            assertNull(redactedNormal.getUids());
            assertNull(redactedNormal.getSsid());
            assertNull(redactedNormal.getUnderlyingNetworks());
            assertEquals(0, redactedNormal.getSubscriptionIds().size());
            assertEquals(WifiInfo.DEFAULT_MAC_ADDRESS,
                    ((WifiInfo) redactedNormal.getTransportInfo()).getBSSID());
            assertEquals(rssi, ((WifiInfo) redactedNormal.getTransportInfo()).getRssi());

            // Uids, ssid, underlying networks & subscriptionIds will be preserved if the given uid
            // holds the associated permissions.
            final NetworkCapabilities redactedPrivileged =
                    redactNc(nc, privilegedUid, privilegedPkg);
            assertEquals(uids, redactedPrivileged.getUids());
            assertEquals(ssid, redactedPrivileged.getSsid());
            assertEquals(List.of(defaultNetwork), redactedPrivileged.getUnderlyingNetworks());
            assertEquals(Set.of(subId1, subId2), redactedPrivileged.getSubscriptionIds());
            assertEquals(bssid, ((WifiInfo) redactedPrivileged.getTransportInfo()).getBSSID());
            assertEquals(rssi, ((WifiInfo) redactedPrivileged.getTransportInfo()).getRssi());

            // The owner uid is only preserved when the network is a VPN and the uid is the
            // same as the owner uid.
            nc.addTransportType(TRANSPORT_VPN);
            assertEquals(normalUid, redactNc(nc, normalUid, normalPkg).getOwnerUid());
            assertEquals(INVALID_UID, redactNc(nc, privilegedUid, privilegedPkg).getOwnerUid());
            nc.removeTransportType(TRANSPORT_VPN);

            // If the given uid doesn't hold location permissions, the owner uid will be set to
            // INVALID_UID even when sent to that UID (this avoids a wifi suggestor knowing where
            // the device is by virtue of the device connecting to its own network).
            assertEquals(INVALID_UID, redactNc(nc, normalUid, normalPkg).getOwnerUid());

            // If the given uid holds location permissions, the owner uid is preserved. This works
            // because the shell holds ACCESS_FINE_LOCATION.
            final int[] administratorUids2 = { privilegedUid };
            nc.setAdministratorUids(administratorUids2);
            nc.setOwnerUid(privilegedUid);
            assertEquals(privilegedUid, redactNc(nc, privilegedUid, privilegedPkg).getOwnerUid());
        });
    }

    /**
     * Tests that connections can be opened on WiFi and cellphone networks,
     * and that they are made from different IP addresses.
     */
    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    @RequiresDevice // Virtual devices use a single internet connection for all networks
    public void testOpenConnection() throws Exception {
        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));

        Network wifiNetwork = mCtsNetUtils.connectToWifi();
        Network cellNetwork = mCtsNetUtils.connectToCell();
        // This server returns the requestor's IP address as the response body.
        URL url = new URL("http://google-ipv6test.appspot.com/ip.js?fmt=text");
        String wifiAddressString = httpGet(wifiNetwork, url);
        String cellAddressString = httpGet(cellNetwork, url);

        assertFalse(String.format("Same address '%s' on two different networks (%s, %s)",
                wifiAddressString, wifiNetwork, cellNetwork),
                wifiAddressString.equals(cellAddressString));

        // Verify that the IP addresses that the requests appeared to come from are actually on the
        // respective networks.
        assertOnNetwork(wifiAddressString, wifiNetwork);
        assertOnNetwork(cellAddressString, cellNetwork);

        assertFalse("Unexpectedly equal: " + wifiNetwork, wifiNetwork.equals(cellNetwork));
    }

    /**
     * Performs a HTTP GET to the specified URL on the specified Network, and returns
     * the response body decoded as UTF-8.
     */
    private static String httpGet(Network network, URL httpUrl) throws IOException {
        HttpURLConnection connection = (HttpURLConnection) network.openConnection(httpUrl);
        try {
            InputStream inputStream = connection.getInputStream();
            return Streams.readFully(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
        } finally {
            connection.disconnect();
        }
    }

    private void assertOnNetwork(String adressString, Network network) throws UnknownHostException {
        InetAddress address = InetAddress.getByName(adressString);
        LinkProperties linkProperties = mCm.getLinkProperties(network);
        // To make sure that the request went out on the right network, check that
        // the IP address seen by the server is assigned to the expected network.
        // We can only do this for IPv6 addresses, because in IPv4 we will likely
        // have a private IPv4 address, and that won't match what the server sees.
        if (address instanceof Inet6Address) {
            assertContains(linkProperties.getAddresses(), address);
        }
    }

    private static<T> void assertContains(Collection<T> collection, T element) {
        assertTrue(element + " not found in " + collection, collection.contains(element));
    }

    private void assertStartUsingNetworkFeatureUnsupported(int networkType, String feature) {
        try {
            mCm.startUsingNetworkFeature(networkType, feature);
            fail("startUsingNetworkFeature is no longer supported in the current API version");
        } catch (UnsupportedOperationException expected) {}
    }

    private void assertStopUsingNetworkFeatureUnsupported(int networkType, String feature) {
        try {
            mCm.startUsingNetworkFeature(networkType, feature);
            fail("stopUsingNetworkFeature is no longer supported in the current API version");
        } catch (UnsupportedOperationException expected) {}
    }

    private void assertRequestRouteToHostUnsupported(int networkType, int hostAddress) {
        try {
            mCm.requestRouteToHost(networkType, hostAddress);
            fail("requestRouteToHost is no longer supported in the current API version");
        } catch (UnsupportedOperationException expected) {}
    }

    @Test
    public void testStartUsingNetworkFeature() {

        final String invalidateFeature = "invalidateFeature";
        final String mmsFeature = "enableMMS";

        assertStartUsingNetworkFeatureUnsupported(TYPE_MOBILE, invalidateFeature);
        assertStopUsingNetworkFeatureUnsupported(TYPE_MOBILE, invalidateFeature);
        assertStartUsingNetworkFeatureUnsupported(TYPE_WIFI, mmsFeature);
    }

    private boolean shouldEthernetBeSupported() {
        // Instant mode apps aren't allowed to query the Ethernet service due to selinux policies.
        // When in instant mode, don't fail if the Ethernet service is available. Instead, rely on
        // the fact that Ethernet should be supported if the device has a hardware Ethernet port, or
        // if the device can be a USB host and thus can use USB Ethernet adapters.
        //
        // Note that this test this will still fail in instant mode if a device supports Ethernet
        // via other hardware means. We are not currently aware of any such device.
        return hasEthernetService()
                || mPackageManager.hasSystemFeature(FEATURE_ETHERNET)
                || mPackageManager.hasSystemFeature(FEATURE_USB_HOST);
    }

    private boolean hasEthernetService() {
        // On Q creating EthernetManager from a thread that does not have a looper (like the test
        // thread) crashes because it tried to use Looper.myLooper() through the default Handler
        // constructor to run onAvailabilityChanged callbacks. Use ServiceManager to check whether
        // the service exists instead.
        // TODO: remove once Q is no longer supported in MTS, as ServiceManager is hidden API
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
            return ServiceManager.getService(Context.ETHERNET_SERVICE) != null;
        }
        return mContext.getSystemService(Context.ETHERNET_SERVICE) != null;
    }

    private boolean shouldBeSupported(int networkType) {
        return mNetworkTypes.contains(networkType)
            || (networkType == ConnectivityManager.TYPE_VPN)
            || (networkType == ConnectivityManager.TYPE_ETHERNET && shouldEthernetBeSupported());
    }

    @Test
    public void testIsNetworkSupported() {
        for (int type = -1; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) {
            boolean supported = mCm.isNetworkSupported(type);
            if (shouldBeSupported(type)) {
                assertTrue("Network type " + type + " should be supported", supported);
            } else {
                assertFalse("Network type " + type + " should not be supported", supported);
            }
        }
    }

    @Test
    public void testRequestRouteToHost() {
        for (int type = -1 ; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) {
            assertRequestRouteToHostUnsupported(type, HOST_ADDRESS);
        }
    }

    @Test
    public void testTest() {
        mCm.getBackgroundDataSetting();
    }

    private NetworkRequest makeDefaultRequest() {
        // Make a request that is similar to the way framework tracks the system
        // default network.
        return new NetworkRequest.Builder()
                .clearCapabilities()
                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
                .addCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                .build();
    }

    private NetworkRequest makeWifiNetworkRequest() {
        return new NetworkRequest.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                .addCapability(NET_CAPABILITY_INTERNET)
                .build();
    }

    private NetworkRequest makeCellNetworkRequest() {
        return new NetworkRequest.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
                .addCapability(NET_CAPABILITY_INTERNET)
                .build();
    }

    private boolean hasPrivateDnsValidated(CallbackEntry entry, Network networkForPrivateDns) {
        if (!networkForPrivateDns.equals(entry.getNetwork())) return false;
        final NetworkCapabilities nc = ((CallbackEntry.CapabilitiesChanged) entry).getCaps();
        return !nc.isPrivateDnsBroken() && nc.hasCapability(NET_CAPABILITY_VALIDATED);
    }

    @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
    public void testIsPrivateDnsBroken() throws InterruptedException {
        final String invalidPrivateDnsServer = "invalidhostname.example.com";
        final String goodPrivateDnsServer = "dns.google";
        mCtsNetUtils.storePrivateDnsSetting();
        final TestableNetworkCallback cb = new TestableNetworkCallback();
        registerNetworkCallback(makeWifiNetworkRequest(), cb);
        try {
            // Verifying the good private DNS sever
            mCtsNetUtils.setPrivateDnsStrictMode(goodPrivateDnsServer);
            final Network networkForPrivateDns =  mCtsNetUtils.ensureWifiConnected();
            cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS,
                    entry -> hasPrivateDnsValidated(entry, networkForPrivateDns));

            // Verifying the broken private DNS sever
            mCtsNetUtils.setPrivateDnsStrictMode(invalidPrivateDnsServer);
            cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS,
                    entry -> (((CallbackEntry.CapabilitiesChanged) entry).getCaps()
                    .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
        } finally {
            mCtsNetUtils.restorePrivateDnsSetting();
            // Toggle wifi to make sure it is re-validated
            reconnectWifi();
        }
    }

    /**
     * Exercises both registerNetworkCallback and unregisterNetworkCallback. This checks to
     * see if we get a callback for the TRANSPORT_WIFI transport type being available.
     *
     * <p>In order to test that a NetworkCallback occurs, we need some change in the network
     * state (either a transport or capability is now available). The most straightforward is
     * WiFi. We could add a version that uses the telephony data connection but it's not clear
     * that it would increase test coverage by much (how many devices have 3G radio but not Wifi?).
     */
    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    public void testRegisterNetworkCallback() throws Exception {
        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));

        // We will register for a WIFI network being available or lost.
        final TestNetworkCallback callback = new TestNetworkCallback();
        registerNetworkCallback(makeWifiNetworkRequest(), callback);

        final TestNetworkCallback defaultTrackingCallback = new TestNetworkCallback();
        registerDefaultNetworkCallback(defaultTrackingCallback);

        final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback();
        final TestNetworkCallback perUidCallback = new TestNetworkCallback();
        final TestNetworkCallback bestMatchingCallback = new TestNetworkCallback();
        final Handler h = new Handler(Looper.getMainLooper());
        if (TestUtils.shouldTestSApis()) {
            runWithShellPermissionIdentity(() -> {
                registerSystemDefaultNetworkCallback(systemDefaultCallback, h);
                registerDefaultNetworkCallbackForUid(Process.myUid(), perUidCallback, h);
            }, NETWORK_SETTINGS);
            registerBestMatchingNetworkCallback(makeDefaultRequest(), bestMatchingCallback, h);
        }

        Network wifiNetwork = null;
        mCtsNetUtils.ensureWifiConnected();

        // Now we should expect to get a network callback about availability of the wifi
        // network even if it was already connected as a state-based action when the callback
        // is registered.
        wifiNetwork = callback.waitForAvailable();
        assertNotNull("Did not receive onAvailable for TRANSPORT_WIFI request",
                wifiNetwork);

        final Network defaultNetwork = defaultTrackingCallback.waitForAvailable();
        assertNotNull("Did not receive onAvailable on default network callback",
                defaultNetwork);

        if (TestUtils.shouldTestSApis()) {
            assertNotNull("Did not receive onAvailable on system default network callback",
                    systemDefaultCallback.waitForAvailable());
            final Network perUidNetwork = perUidCallback.waitForAvailable();
            assertNotNull("Did not receive onAvailable on per-UID default network callback",
                    perUidNetwork);
            assertEquals(defaultNetwork, perUidNetwork);
            final Network bestMatchingNetwork = bestMatchingCallback.waitForAvailable();
            assertNotNull("Did not receive onAvailable on best matching network callback",
                    bestMatchingNetwork);
            assertEquals(defaultNetwork, bestMatchingNetwork);
        }
    }

    /**
     * Tests both registerNetworkCallback and unregisterNetworkCallback similarly to
     * {@link #testRegisterNetworkCallback} except that a {@code PendingIntent} is used instead
     * of a {@code NetworkCallback}.
     */
    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    public void testRegisterNetworkCallback_withPendingIntent() {
        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));

        // Create a ConnectivityActionReceiver that has an IntentFilter for our locally defined
        // action, NETWORK_CALLBACK_ACTION.
        final IntentFilter filter = new IntentFilter();
        filter.addAction(NETWORK_CALLBACK_ACTION);

        final ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
                mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED);
        mContext.registerReceiver(receiver, filter);

        // Create a broadcast PendingIntent for NETWORK_CALLBACK_ACTION.
        final Intent intent = new Intent(NETWORK_CALLBACK_ACTION)
                .setPackage(mContext.getPackageName());
        // While ConnectivityService would put extra info such as network or request id before
        // broadcasting the inner intent. The MUTABLE flag needs to be added accordingly.
        // TODO: replace with PendingIntent.FLAG_MUTABLE when this code compiles against S+ or
        //  shims.
        final int pendingIntentFlagMutable = 1 << 25;
        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0 /*requestCode*/,
                intent, PendingIntent.FLAG_CANCEL_CURRENT | pendingIntentFlagMutable);

        // We will register for a WIFI network being available or lost.
        mCm.registerNetworkCallback(makeWifiNetworkRequest(), pendingIntent);

        try {
            mCtsNetUtils.ensureWifiConnected();

            // Now we expect to get the Intent delivered notifying of the availability of the wifi
            // network even if it was already connected as a state-based action when the callback
            // is registered.
            assertTrue("Did not receive expected Intent " + intent + " for TRANSPORT_WIFI",
                    receiver.waitForState());
        } catch (InterruptedException e) {
            fail("Broadcast receiver or NetworkCallback wait was interrupted.");
        } finally {
            mCm.unregisterNetworkCallback(pendingIntent);
            pendingIntent.cancel();
            mContext.unregisterReceiver(receiver);
        }
    }

    private void runIdenticalPendingIntentsRequestTest(boolean useListen) throws Exception {
        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));

        // Disconnect before registering callbacks, reconnect later to fire them
        mCtsNetUtils.ensureWifiDisconnected(null);

        final NetworkRequest firstRequest = makeWifiNetworkRequest();
        final NetworkRequest secondRequest = new NetworkRequest(firstRequest);
        // Will match wifi or test, since transports are ORed; but there should only be wifi
        secondRequest.networkCapabilities.addTransportType(TRANSPORT_TEST);

        PendingIntent firstIntent = null;
        PendingIntent secondIntent = null;
        BroadcastReceiver receiver = null;

        // Avoid receiving broadcasts from other runs by appending a timestamp
        final String broadcastAction = NETWORK_CALLBACK_ACTION + System.currentTimeMillis();
        try {
            // TODO: replace with PendingIntent.FLAG_MUTABLE when this code compiles against S+
            // Intent is mutable to receive EXTRA_NETWORK_REQUEST from ConnectivityService
            final int pendingIntentFlagMutable = 1 << 25;
            final String extraBoolKey = "extra_bool";
            firstIntent = PendingIntent.getBroadcast(mContext,
                    0 /* requestCode */,
                    new Intent(broadcastAction).putExtra(extraBoolKey, false),
                    PendingIntent.FLAG_UPDATE_CURRENT | pendingIntentFlagMutable);

            if (useListen) {
                mCm.registerNetworkCallback(firstRequest, firstIntent);
            } else {
                mCm.requestNetwork(firstRequest, firstIntent);
            }

            // Second intent equals the first as per filterEquals (extras don't count), so first
            // intent will be updated with the new extras
            secondIntent = PendingIntent.getBroadcast(mContext,
                    0 /* requestCode */,
                    new Intent(broadcastAction).putExtra(extraBoolKey, true),
                    PendingIntent.FLAG_UPDATE_CURRENT | pendingIntentFlagMutable);

            // Because secondIntent.intentFilterEquals the first, the request should be replaced
            if (useListen) {
                mCm.registerNetworkCallback(secondRequest, secondIntent);
            } else {
                mCm.requestNetwork(secondRequest, secondIntent);
            }

            final IntentFilter filter = new IntentFilter();
            filter.addAction(broadcastAction);

            final CompletableFuture<Network> networkFuture = new CompletableFuture<>();
            final AtomicInteger receivedCount = new AtomicInteger(0);
            receiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    final NetworkRequest request = intent.getParcelableExtra(EXTRA_NETWORK_REQUEST);
                    assertPendingIntentRequestMatches(request, secondRequest, useListen);
                    receivedCount.incrementAndGet();
                    networkFuture.complete(intent.getParcelableExtra(EXTRA_NETWORK));
                }
            };
            mContext.registerReceiver(receiver, filter);

            final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
            try {
                assertEquals(wifiNetwork, networkFuture.get(
                        NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
            } catch (TimeoutException e) {
                throw new AssertionError("PendingIntent not received for " + secondRequest, e);
            }

            // Sleep for a small amount of time to try to check that only one callback is ever
            // received (so the first callback was really unregistered). This does not guarantee
            // that the test will fail if it runs very slowly, but it should at least be very
            // noticeably flaky.
            Thread.sleep(NO_CALLBACK_TIMEOUT_MS);

            // For R- frameworks, listens will receive duplicated callbacks. See b/189868426.
            if (isAtLeastS() || !useListen) {
                assertEquals("PendingIntent should only be received once", 1, receivedCount.get());
            }
        } finally {
            if (firstIntent != null) mCm.unregisterNetworkCallback(firstIntent);
            if (secondIntent != null) mCm.unregisterNetworkCallback(secondIntent);
            if (receiver != null) mContext.unregisterReceiver(receiver);
            mCtsNetUtils.ensureWifiConnected();
        }
    }

    private void assertPendingIntentRequestMatches(NetworkRequest broadcasted, NetworkRequest filed,
            boolean useListen) {
        assertArrayEquals(filed.networkCapabilities.getCapabilities(),
                broadcasted.networkCapabilities.getCapabilities());
        // For R- frameworks, listens will receive duplicated callbacks. See b/189868426.
        if (!isAtLeastS() && useListen) return;
        assertArrayEquals(filed.networkCapabilities.getTransportTypes(),
                broadcasted.networkCapabilities.getTransportTypes());
    }

    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    public void testRegisterNetworkRequest_identicalPendingIntents() throws Exception {
        runIdenticalPendingIntentsRequestTest(false /* useListen */);
    }

    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    public void testRegisterNetworkCallback_identicalPendingIntents() throws Exception {
        runIdenticalPendingIntentsRequestTest(true /* useListen */);
    }

    /**
     * Exercises the requestNetwork with NetworkCallback API. This checks to
     * see if we get a callback for an INTERNET request.
     */
    @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
    @Test
    public void testRequestNetworkCallback() throws Exception {
        final TestNetworkCallback callback = new TestNetworkCallback();
        requestNetwork(new NetworkRequest.Builder()
                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                .build(), callback);

        // Wait to get callback for availability of internet
        Network internetNetwork = callback.waitForAvailable();
        assertNotNull("Did not receive NetworkCallback#onAvailable for INTERNET", internetNetwork);
    }

    /**
     * Exercises the requestNetwork with NetworkCallback API with timeout - expected to
     * fail. Use WIFI and switch Wi-Fi off.
     */
    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    public void testRequestNetworkCallback_onUnavailable() {
        final boolean previousWifiEnabledState = mWifiManager.isWifiEnabled();
        if (previousWifiEnabledState) {
            mCtsNetUtils.ensureWifiDisconnected(null);
        }

        final TestNetworkCallback callback = new TestNetworkCallback();
        requestNetwork(new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(),
                callback, 100);

        try {
            // Wait to get callback for unavailability of requested network
            assertTrue("Did not receive NetworkCallback#onUnavailable",
                    callback.waitForUnavailable());
        } catch (InterruptedException e) {
            fail("NetworkCallback wait was interrupted.");
        } finally {
            if (previousWifiEnabledState) {
                mCtsNetUtils.connectToWifi();
            }
        }
    }

    private InetAddress getFirstV4Address(Network network) {
        LinkProperties linkProperties = mCm.getLinkProperties(network);
        for (InetAddress address : linkProperties.getAddresses()) {
            if (address instanceof Inet4Address) {
                return address;
            }
        }
        return null;
    }

    /**
     * Checks that enabling/disabling wifi causes CONNECTIVITY_ACTION broadcasts.
     */
    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    public void testToggleWifiConnectivityAction() throws Exception {
        // toggleWifi calls connectToWifi and disconnectFromWifi, which both wait for
        // CONNECTIVITY_ACTION broadcasts.
        mCtsNetUtils.toggleWifi();
    }

    /** Verify restricted networks cannot be requested. */
    @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
    @Test
    public void testRestrictedNetworks() {
        // Verify we can request unrestricted networks:
        NetworkRequest request = new NetworkRequest.Builder()
                .addCapability(NET_CAPABILITY_INTERNET).build();
        NetworkCallback callback = new NetworkCallback();
        mCm.requestNetwork(request, callback);
        mCm.unregisterNetworkCallback(callback);
        // Verify we cannot request restricted networks:
        request = new NetworkRequest.Builder().addCapability(NET_CAPABILITY_IMS).build();
        callback = new NetworkCallback();
        try {
            mCm.requestNetwork(request, callback);
            fail("No exception thrown when restricted network requested.");
        } catch (SecurityException expected) {}
    }

    // Returns "true", "false" or "none"
    private String getWifiMeteredStatus(String ssid) throws Exception {
        // Interestingly giving the SSID as an argument to list wifi-networks
        // only works iff the network in question has the "false" policy.
        // Also unfortunately runShellCommand does not pass the command to the interpreter
        // so it's not possible to | grep the ssid.
        final String command = "cmd netpolicy list wifi-networks";
        final String policyString = runShellCommand(mInstrumentation, command);

        final Matcher m = Pattern.compile("^" + ssid + ";(true|false|none)$",
                Pattern.MULTILINE | Pattern.UNIX_LINES).matcher(policyString);
        if (!m.find()) {
            fail("Unexpected format from cmd netpolicy, policyString = " + policyString);
        }
        return m.group(1);
    }

    // metered should be "true", "false" or "none"
    private void setWifiMeteredStatus(String ssid, String metered) throws Exception {
        final String setCommand = "cmd netpolicy set metered-network " + ssid + " " + metered;
        runShellCommand(mInstrumentation, setCommand);
        assertEquals(getWifiMeteredStatus(ssid), metered);
    }

    private String unquoteSSID(String ssid) {
        // SSID is returned surrounded by quotes if it can be decoded as UTF-8.
        // Otherwise it's guaranteed not to start with a quote.
        if (ssid.charAt(0) == '"') {
            return ssid.substring(1, ssid.length() - 1);
        } else {
            return ssid;
        }
    }

    private Network waitForActiveNetworkMetered(final int targetTransportType,
            final boolean requestedMeteredness, final boolean waitForValidation,
            final boolean useSystemDefault)
            throws Exception {
        final CompletableFuture<Network> networkFuture = new CompletableFuture<>();
        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);
                final boolean validated = nc.hasCapability(NET_CAPABILITY_VALIDATED);
                if (metered == requestedMeteredness && (!waitForValidation || validated)) {
                    networkFuture.complete(network);
                }
            }
        };

        try {
            // 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'll wait for the setting to change.
            if (useSystemDefault) {
                runWithShellPermissionIdentity(() ->
                                registerSystemDefaultNetworkCallback(networkCallback,
                                        new Handler(Looper.getMainLooper())),
                        NETWORK_SETTINGS);
            } else {
                registerDefaultNetworkCallback(networkCallback);
            }

            // Changing meteredness on wifi involves reconnecting, which can take several seconds
            // (involves re-associating, DHCP...).
            return networkFuture.get(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            throw new AssertionError("Timed out waiting for active network metered status to "
                    + "change to " + requestedMeteredness + " ; network = "
                    + mCm.getActiveNetwork(), e);
        }
    }

    private Network setWifiMeteredStatusAndWait(String ssid, boolean isMetered,
            boolean waitForValidation) throws Exception {
        setWifiMeteredStatus(ssid, Boolean.toString(isMetered) /* metered */);
        mCtsNetUtils.ensureWifiConnected();
        return waitForActiveNetworkMetered(TRANSPORT_WIFI,
                isMetered /* requestedMeteredness */,
                waitForValidation,
                true /* useSystemDefault */);
    }

    private void assertMultipathPreferenceIsEventually(Network network, int oldValue,
            int expectedValue) {
        // Quick check : if oldValue == expectedValue, there is no way to guarantee the test
        // is not flaky.
        assertNotSame(oldValue, expectedValue);

        for (int i = 0; i < NUM_TRIES_MULTIPATH_PREF_CHECK; ++i) {
            final int actualValue = mCm.getMultipathPreference(network);
            if (actualValue == expectedValue) {
                return;
            }
            if (actualValue != oldValue) {
                fail("Multipath preference is neither previous (" + oldValue
                        + ") nor expected (" + expectedValue + ")");
            }
            SystemClock.sleep(INTERVAL_MULTIPATH_PREF_CHECK_MS);
        }
        fail("Timed out waiting for multipath preference to change. expected = "
                + expectedValue + " ; actual = " + mCm.getMultipathPreference(network));
    }

    private int getCurrentMeteredMultipathPreference(ContentResolver resolver) {
        final String rawMeteredPref = Settings.Global.getString(resolver,
                NETWORK_METERED_MULTIPATH_PREFERENCE);
        return TextUtils.isEmpty(rawMeteredPref)
            ? getIntResourceForName(NETWORK_METERED_MULTIPATH_PREFERENCE_RES_NAME)
            : Integer.parseInt(rawMeteredPref);
    }

    private int findNextPrefValue(ContentResolver resolver) {
        // A bit of a nuclear hammer, but race conditions in CTS are bad. To be able to
        // detect a correct setting value without race conditions, the next pref must
        // be a valid value (range 0..3) that is different from the old setting of the
        // metered preference and from the unmetered preference.
        final int meteredPref = getCurrentMeteredMultipathPreference(resolver);
        final int unmeteredPref = ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED;
        if (0 != meteredPref && 0 != unmeteredPref) return 0;
        if (1 != meteredPref && 1 != unmeteredPref) return 1;
        return 2;
    }

    /**
     * Verify that getMultipathPreference does return appropriate values
     * for metered and unmetered networks.
     */
    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    public void testGetMultipathPreference() throws Exception {
        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
        final ContentResolver resolver = mContext.getContentResolver();
        mCtsNetUtils.ensureWifiConnected();
        final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
        final String oldMeteredSetting = getWifiMeteredStatus(ssid);
        final String oldMeteredMultipathPreference = Settings.Global.getString(
                resolver, NETWORK_METERED_MULTIPATH_PREFERENCE);
        try {
            final int initialMeteredPreference = getCurrentMeteredMultipathPreference(resolver);
            int newMeteredPreference = findNextPrefValue(resolver);
            Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE,
                    Integer.toString(newMeteredPreference));
            // Wifi meteredness changes from unmetered to metered will disconnect and reconnect
            // since R.
            final Network network = setWifiMeteredStatusAndWait(ssid, true /* isMetered */,
                    false /* waitForValidation */);
            assertEquals(ssid, unquoteSSID(mWifiManager.getConnectionInfo().getSSID()));
            assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
                    NET_CAPABILITY_NOT_METERED), false);
            assertMultipathPreferenceIsEventually(network, initialMeteredPreference,
                    newMeteredPreference);

            final int oldMeteredPreference = newMeteredPreference;
            newMeteredPreference = findNextPrefValue(resolver);
            Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE,
                    Integer.toString(newMeteredPreference));
            assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
                    NET_CAPABILITY_NOT_METERED), false);
            assertMultipathPreferenceIsEventually(network,
                    oldMeteredPreference, newMeteredPreference);

            // No disconnect from unmetered to metered.
            setWifiMeteredStatusAndWait(ssid, false /* isMetered */, false /* waitForValidation */);
            assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
                    NET_CAPABILITY_NOT_METERED), true);
            assertMultipathPreferenceIsEventually(network, newMeteredPreference,
                    ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED);
        } finally {
            Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE,
                    oldMeteredMultipathPreference);
            setWifiMeteredStatus(ssid, oldMeteredSetting);
        }
    }

    // TODO: move the following socket keep alive test to dedicated test class.
    /**
     * Callback used in tcp keepalive offload that allows caller to wait callback fires.
     */
    private static class TestSocketKeepaliveCallback extends SocketKeepalive.Callback {
        public enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR };

        public static class CallbackValue {
            public final CallbackType callbackType;
            public final int error;

            private CallbackValue(final CallbackType type, final int error) {
                this.callbackType = type;
                this.error = error;
            }

            public static class OnStartedCallback extends CallbackValue {
                OnStartedCallback() { super(CallbackType.ON_STARTED, 0); }
            }

            public static class OnStoppedCallback extends CallbackValue {
                OnStoppedCallback() { super(CallbackType.ON_STOPPED, 0); }
            }

            public static class OnErrorCallback extends CallbackValue {
                OnErrorCallback(final int error) { super(CallbackType.ON_ERROR, error); }
            }

            @Override
            public boolean equals(Object o) {
                return o.getClass() == this.getClass()
                        && this.callbackType == ((CallbackValue) o).callbackType
                        && this.error == ((CallbackValue) o).error;
            }

            @Override
            public String toString() {
                return String.format("%s(%s, %d)", getClass().getSimpleName(), callbackType, error);
            }
        }

        private final LinkedBlockingQueue<CallbackValue> mCallbacks = new LinkedBlockingQueue<>();

        @Override
        public void onStarted() {
            mCallbacks.add(new CallbackValue.OnStartedCallback());
        }

        @Override
        public void onStopped() {
            mCallbacks.add(new CallbackValue.OnStoppedCallback());
        }

        @Override
        public void onError(final int error) {
            mCallbacks.add(new CallbackValue.OnErrorCallback(error));
        }

        public CallbackValue pollCallback() {
            try {
                return mCallbacks.poll(KEEPALIVE_CALLBACK_TIMEOUT_MS,
                        TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
                fail("Callback not seen after " + KEEPALIVE_CALLBACK_TIMEOUT_MS + " ms");
            }
            return null;
        }
        private void expectCallback(CallbackValue expectedCallback) {
            final CallbackValue actualCallback = pollCallback();
            assertEquals(expectedCallback, actualCallback);
        }

        public void expectStarted() {
            expectCallback(new CallbackValue.OnStartedCallback());
        }

        public void expectStopped() {
            expectCallback(new CallbackValue.OnStoppedCallback());
        }

        public void expectError(int error) {
            expectCallback(new CallbackValue.OnErrorCallback(error));
        }
    }

    private InetAddress getAddrByName(final String hostname, final int family) throws Exception {
        final InetAddress[] allAddrs = InetAddress.getAllByName(hostname);
        for (InetAddress addr : allAddrs) {
            if (family == AF_INET && addr instanceof Inet4Address) return addr;

            if (family == AF_INET6 && addr instanceof Inet6Address) return addr;

            if (family == AF_UNSPEC) return addr;
        }
        return null;
    }

    private Socket getConnectedSocket(final Network network, final String host, final int port,
            final int family) throws Exception {
        final Socket s = network.getSocketFactory().createSocket();
        try {
            final InetAddress addr = getAddrByName(host, family);
            if (addr == null) fail("Fail to get destination address for " + family);

            final InetSocketAddress sockAddr = new InetSocketAddress(addr, port);
            s.connect(sockAddr);
        } catch (Exception e) {
            s.close();
            throw e;
        }
        return s;
    }

    private int getSupportedKeepalivesForNet(@NonNull Network network) throws Exception {
        final NetworkCapabilities nc = mCm.getNetworkCapabilities(network);

        // Get number of supported concurrent keepalives for testing network.
        final int[] keepalivesPerTransport = KeepaliveUtils.getSupportedKeepalives(mContext);
        return KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(
                keepalivesPerTransport, nc);
    }

    private static boolean isTcpKeepaliveSupportedByKernel() {
        final String kVersionString = VintfRuntimeInfo.getKernelVersion();
        return DeviceInfoUtils.compareMajorMinorVersion(kVersionString, "4.8") >= 0;
    }

    /**
     * Verifies that the keepalive API cannot create any keepalive when the maximum number of
     * keepalives is set to 0.
     */
    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    public void testKeepaliveWifiUnsupported() throws Exception {
        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));

        final Network network = mCtsNetUtils.ensureWifiConnected();
        if (getSupportedKeepalivesForNet(network) != 0) return;
        final InetAddress srcAddr = getFirstV4Address(network);
        assumeTrue("This test requires native IPv4", srcAddr != null);

        runWithShellPermissionIdentity(() -> {
            assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 1, 0));
            assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 0, 1));
        });
    }

    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    @RequiresDevice // Keepalive is not supported on virtual hardware
    public void testCreateTcpKeepalive() throws Exception {
        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));

        final Network network = mCtsNetUtils.ensureWifiConnected();
        if (getSupportedKeepalivesForNet(network) == 0) return;
        final InetAddress srcAddr = getFirstV4Address(network);
        assumeTrue("This test requires native IPv4", srcAddr != null);

        // If kernel < 4.8 then it doesn't support TCP keepalive, but it might still support
        // NAT-T keepalive. If keepalive limits from resource overlay is not zero, TCP keepalive
        // needs to be supported except if the kernel doesn't support it.
        if (!isTcpKeepaliveSupportedByKernel()) {
            // Verify that the callback result is expected.
            runWithShellPermissionIdentity(() -> {
                assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 0, 1));
            });
            Log.i(TAG, "testCreateTcpKeepalive is skipped for kernel "
                    + VintfRuntimeInfo.getKernelVersion());
            return;
        }

        final byte[] requestBytes = CtsNetUtils.HTTP_REQUEST.getBytes("UTF-8");
        // So far only ipv4 tcp keepalive offload is supported.
        // TODO: add test case for ipv6 tcp keepalive offload when it is supported.
        try (Socket s = getConnectedSocket(network, TEST_HOST, HTTP_PORT, AF_INET)) {

            // Should able to start keep alive offload when socket is idle.
            final Executor executor = mContext.getMainExecutor();
            final TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback();

            mUiAutomation.adoptShellPermissionIdentity();
            try (SocketKeepalive sk = mCm.createSocketKeepalive(network, s, executor, callback)) {
                sk.start(MIN_KEEPALIVE_INTERVAL);
                callback.expectStarted();

                // App should not able to write during keepalive offload.
                final OutputStream out = s.getOutputStream();
                try {
                    out.write(requestBytes);
                    fail("Should not able to write");
                } catch (IOException e) { }
                // App should not able to read during keepalive offload.
                final InputStream in = s.getInputStream();
                byte[] responseBytes = new byte[4096];
                try {
                    in.read(responseBytes);
                    fail("Should not able to read");
                } catch (IOException e) { }

                // Stop.
                sk.stop();
                callback.expectStopped();
            } finally {
                mUiAutomation.dropShellPermissionIdentity();
            }

            // Ensure socket is still connected.
            assertTrue(s.isConnected());
            assertFalse(s.isClosed());

            // Let socket be not idle.
            try {
                final OutputStream out = s.getOutputStream();
                out.write(requestBytes);
            } catch (IOException e) {
                fail("Failed to write data " + e);
            }
            // Make sure response data arrives.
            final MessageQueue fdHandlerQueue = Looper.getMainLooper().getQueue();
            final FileDescriptor fd = s.getFileDescriptor$();
            final CountDownLatch mOnReceiveLatch = new CountDownLatch(1);
            fdHandlerQueue.addOnFileDescriptorEventListener(fd, EVENT_INPUT, (readyFd, events) -> {
                mOnReceiveLatch.countDown();
                return 0; // Unregister listener.
            });
            if (!mOnReceiveLatch.await(2, TimeUnit.SECONDS)) {
                fdHandlerQueue.removeOnFileDescriptorEventListener(fd);
                fail("Timeout: no response data");
            }

            // Should get ERROR_SOCKET_NOT_IDLE because there is still data in the receive queue
            // that has not been read.
            mUiAutomation.adoptShellPermissionIdentity();
            try (SocketKeepalive sk = mCm.createSocketKeepalive(network, s, executor, callback)) {
                sk.start(MIN_KEEPALIVE_INTERVAL);
                callback.expectError(SocketKeepalive.ERROR_SOCKET_NOT_IDLE);
            } finally {
                mUiAutomation.dropShellPermissionIdentity();
            }
        }
    }

    private ArrayList<SocketKeepalive> createConcurrentKeepalivesOfType(
            int requestCount, @NonNull TestSocketKeepaliveCallback callback,
            Supplier<SocketKeepalive> kaFactory) {
        final ArrayList<SocketKeepalive> kalist = new ArrayList<>();

        int remainingRetries = MAX_KEEPALIVE_RETRY_COUNT;

        // Test concurrent keepalives with the given supplier.
        while (kalist.size() < requestCount) {
            final SocketKeepalive ka = kaFactory.get();
            ka.start(MIN_KEEPALIVE_INTERVAL);
            TestSocketKeepaliveCallback.CallbackValue cv = callback.pollCallback();
            assertNotNull(cv);
            if (cv.callbackType == TestSocketKeepaliveCallback.CallbackType.ON_ERROR) {
                if (kalist.size() == 0 && cv.error == SocketKeepalive.ERROR_UNSUPPORTED) {
                    // Unsupported.
                    break;
                } else if (cv.error == SocketKeepalive.ERROR_INSUFFICIENT_RESOURCES) {
                    // Limit reached or temporary unavailable due to stopped slot is not yet
                    // released.
                    if (remainingRetries > 0) {
                        SystemClock.sleep(INTERVAL_KEEPALIVE_RETRY_MS);
                        remainingRetries--;
                        continue;
                    }
                    break;
                }
            }
            if (cv.callbackType == TestSocketKeepaliveCallback.CallbackType.ON_STARTED) {
                kalist.add(ka);
            } else {
                fail("Unexpected error when creating " + (kalist.size() + 1) + " "
                        + ka.getClass().getSimpleName() + ": " + cv);
            }
        }

        return kalist;
    }

    private @NonNull ArrayList<SocketKeepalive> createConcurrentNattSocketKeepalives(
            @NonNull Network network, @NonNull InetAddress srcAddr, int requestCount,
            @NonNull TestSocketKeepaliveCallback callback)  throws Exception {

        final Executor executor = mContext.getMainExecutor();

        // Initialize a real NaT-T socket.
        final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE);
        final UdpEncapsulationSocket nattSocket = mIpSec.openUdpEncapsulationSocket();
        final InetAddress dstAddr = getAddrByName(TEST_HOST, AF_INET);
        assertNotNull(srcAddr);
        assertNotNull(dstAddr);

        // Test concurrent Nat-T keepalives.
        final ArrayList<SocketKeepalive> result = createConcurrentKeepalivesOfType(requestCount,
                callback, () -> mCm.createSocketKeepalive(network, nattSocket,
                        srcAddr, dstAddr, executor, callback));

        nattSocket.close();
        return result;
    }

    private @NonNull ArrayList<SocketKeepalive> createConcurrentTcpSocketKeepalives(
            @NonNull Network network, int requestCount,
            @NonNull TestSocketKeepaliveCallback callback) {
        final Executor executor = mContext.getMainExecutor();

        // Create concurrent TCP keepalives.
        return createConcurrentKeepalivesOfType(requestCount, callback, () -> {
            // Assert that TCP connections can be established. The file descriptor of tcp
            // sockets will be duplicated and kept valid in service side if the keepalives are
            // successfully started.
            try (Socket tcpSocket = getConnectedSocket(network, TEST_HOST, HTTP_PORT,
                    AF_INET)) {
                return mCm.createSocketKeepalive(network, tcpSocket, executor, callback);
            } catch (Exception e) {
                fail("Unexpected error when creating TCP socket: " + e);
            }
            return null;
        });
    }

    /**
     * Creates concurrent keepalives until the specified counts of each type of keepalives are
     * reached or the expected error callbacks are received for each type of keepalives.
     *
     * @return the total number of keepalives created.
     */
    private int createConcurrentSocketKeepalives(
            @NonNull Network network, @NonNull InetAddress srcAddr, int nattCount, int tcpCount)
            throws Exception {
        final ArrayList<SocketKeepalive> kalist = new ArrayList<>();
        final TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback();

        kalist.addAll(createConcurrentNattSocketKeepalives(network, srcAddr, nattCount, callback));
        kalist.addAll(createConcurrentTcpSocketKeepalives(network, tcpCount, callback));

        final int ret = kalist.size();

        // Clean up.
        for (final SocketKeepalive ka : kalist) {
            ka.stop();
            callback.expectStopped();
        }
        kalist.clear();

        return ret;
    }

    /**
     * Verifies that the concurrent keepalive slots meet the minimum requirement, and don't
     * get leaked after iterations.
     */
    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    @RequiresDevice // Keepalive is not supported on virtual hardware
    public void testSocketKeepaliveLimitWifi() throws Exception {
        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));

        final Network network = mCtsNetUtils.ensureWifiConnected();
        final int supported = getSupportedKeepalivesForNet(network);
        if (supported == 0) {
            return;
        }
        final InetAddress srcAddr = getFirstV4Address(network);
        assumeTrue("This test requires native IPv4", srcAddr != null);

        runWithShellPermissionIdentity(() -> {
            // Verifies that the supported keepalive slots meet MIN_SUPPORTED_KEEPALIVE_COUNT.
            assertGreaterOrEqual(supported, MIN_SUPPORTED_WIFI_KEEPALIVE_COUNT);

            // Verifies that Nat-T keepalives can be established.
            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
                    supported + 1, 0));
            // Verifies that keepalives don't get leaked in second round.
            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, supported,
                    0));
        });

        // If kernel < 4.8 then it doesn't support TCP keepalive, but it might still support
        // NAT-T keepalive. Test below cases only if TCP keepalive is supported by kernel.
        if (!isTcpKeepaliveSupportedByKernel()) return;

        runWithShellPermissionIdentity(() -> {
            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, 0,
                    supported + 1));

            // Verifies that different types can be established at the same time.
            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
                    supported / 2, supported - supported / 2));

            // Verifies that keepalives don't get leaked in second round.
            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, 0,
                    supported));
            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
                    supported / 2, supported - supported / 2));
        });
    }

    /**
     * Verifies that the concurrent keepalive slots meet the minimum telephony requirement, and
     * don't get leaked after iterations.
     */
    @AppModeFull(reason = "Cannot request network in instant app mode")
    @Test
    @RequiresDevice // Keepalive is not supported on virtual hardware
    public void testSocketKeepaliveLimitTelephony() throws Exception {
        if (!mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)) {
            Log.i(TAG, "testSocketKeepaliveLimitTelephony cannot execute unless device"
                    + " supports telephony");
            return;
        }

        final int firstSdk = SdkLevel.isAtLeastS()
                ? Build.VERSION.DEVICE_INITIAL_SDK_INT
                // FIRST_SDK_INT was a @TestApi field renamed to DEVICE_INITIAL_SDK_INT in S
                : Build.VERSION.class.getField("FIRST_SDK_INT").getInt(null);
        if (firstSdk < Build.VERSION_CODES.Q) {
            Log.i(TAG, "testSocketKeepaliveLimitTelephony: skip test for devices launching"
                    + " before Q: " + firstSdk);
            return;
        }

        final Network network = mCtsNetUtils.connectToCell();
        final int supported = getSupportedKeepalivesForNet(network);
        final InetAddress srcAddr = getFirstV4Address(network);
        assumeTrue("This test requires native IPv4", srcAddr != null);

        runWithShellPermissionIdentity(() -> {
            // Verifies that the supported keepalive slots meet minimum requirement.
            assertGreaterOrEqual(supported, MIN_SUPPORTED_CELLULAR_KEEPALIVE_COUNT);
            // Verifies that Nat-T keepalives can be established.
            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr,
                    supported + 1, 0));
            // Verifies that keepalives don't get leaked in second round.
            assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, supported,
                    0));
        });
    }

    private int getIntResourceForName(@NonNull String resName) {
        final Resources r = mContext.getResources();
        final int resId = r.getIdentifier(resName, "integer", "android");
        return r.getInteger(resId);
    }

    /**
     * Verifies that the keepalive slots are limited as customized for unprivileged requests.
     */
    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    @RequiresDevice // Keepalive is not supported on virtual hardware
    public void testSocketKeepaliveUnprivileged() throws Exception {
        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));

        final Network network = mCtsNetUtils.ensureWifiConnected();
        final int supported = getSupportedKeepalivesForNet(network);
        if (supported == 0) {
            return;
        }
        final InetAddress srcAddr = getFirstV4Address(network);
        assumeTrue("This test requires native IPv4", srcAddr != null);

        // Resource ID might be shifted on devices that compiled with different symbols.
        // Thus, resolve ID at runtime is needed.
        final int allowedUnprivilegedPerUid =
                getIntResourceForName(KEEPALIVE_ALLOWED_UNPRIVILEGED_RES_NAME);
        final int reservedPrivilegedSlots =
                getIntResourceForName(KEEPALIVE_RESERVED_PER_SLOT_RES_NAME);
        // Verifies that unprivileged request per uid cannot exceed the limit customized in the
        // resource. Currently, unprivileged keepalive slots are limited to Nat-T only, this test
        // does not apply to TCP.
        assertGreaterOrEqual(supported, reservedPrivilegedSlots);
        assertGreaterOrEqual(supported, allowedUnprivilegedPerUid);
        final int expectedUnprivileged =
                Math.min(allowedUnprivilegedPerUid, supported - reservedPrivilegedSlots);
        assertEquals(expectedUnprivileged,
                createConcurrentSocketKeepalives(network, srcAddr, supported + 1, 0));
    }

    private static void assertGreaterOrEqual(long greater, long lesser) {
        assertTrue("" + greater + " expected to be greater than or equal to " + lesser,
                greater >= lesser);
    }

    private void verifyBindSocketToRestrictedNetworkDisallowed() throws Exception {
        final TestableNetworkCallback testNetworkCb = new TestableNetworkCallback();
        final NetworkRequest testRequest = new NetworkRequest.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
                .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                        TEST_RESTRICTED_NW_IFACE_NAME))
                .build();
        runWithShellPermissionIdentity(() -> requestNetwork(testRequest, testNetworkCb),
                CONNECTIVITY_USE_RESTRICTED_NETWORKS,
                // CONNECTIVITY_INTERNAL is for requesting restricted network because shell does not
                // have CONNECTIVITY_USE_RESTRICTED_NETWORKS on R.
                CONNECTIVITY_INTERNAL);

        // Create a restricted network and ensure this package cannot bind to that network either.
        final NetworkAgent agent = createRestrictedNetworkAgent(mContext);
        final Network network = agent.getNetwork();

        try (Socket socket = new Socket()) {
            // Verify that the network is restricted.
            testNetworkCb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
                    NETWORK_CALLBACK_TIMEOUT_MS,
                    entry -> network.equals(entry.getNetwork())
                            && (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
                            .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)));
            // CtsNetTestCases package doesn't hold CONNECTIVITY_USE_RESTRICTED_NETWORKS, so it
            // does not allow to bind socket to restricted network.
            assertThrows(IOException.class, () -> network.bindSocket(socket));
        } finally {
            agent.unregister();
        }
    }

    /**
     * Verifies that apps are not allowed to access restricted networks even if they declare the
     * CONNECTIVITY_USE_RESTRICTED_NETWORKS permission in their manifests.
     * See. b/144679405.
     */
    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    @IgnoreUpTo(Build.VERSION_CODES.Q)
    public void testRestrictedNetworkPermission() throws Exception {
        // Ensure that CONNECTIVITY_USE_RESTRICTED_NETWORKS isn't granted to this package.
        final PackageInfo app = mPackageManager.getPackageInfo(mContext.getPackageName(),
                GET_PERMISSIONS);
        final int index = ArrayUtils.indexOf(
                app.requestedPermissions, CONNECTIVITY_USE_RESTRICTED_NETWORKS);
        assertTrue(index >= 0);
        assertTrue(app.requestedPermissionsFlags[index] != PERMISSION_GRANTED);

        if (mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
            // Expect binding to the wifi network to succeed.
            final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
            try (Socket socket = new Socket()) {
                wifiNetwork.bindSocket(socket);
            }
        }

        // Ensure that this package cannot bind to any restricted network that's currently
        // connected.
        Network[] networks = mCm.getAllNetworks();
        for (Network network : networks) {
            final NetworkCapabilities nc = mCm.getNetworkCapabilities(network);
            if (nc == null) {
                continue;
            }

            try (Socket socket = new Socket()) {
                if (nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
                    network.bindSocket(socket);  // binding should succeed
                } else {
                    assertThrows(IOException.class, () -> network.bindSocket(socket));
                }
            }
        }

        verifyBindSocketToRestrictedNetworkDisallowed();
    }

    /**
     * Verifies that apps are allowed to call setAirplaneMode if they declare
     * NETWORK_AIRPLANE_MODE permission in their manifests.
     * See b/145164696.
     */
    @AppModeFull(reason = "NETWORK_AIRPLANE_MODE permission can't be granted to instant apps")
    @Test
    public void testSetAirplaneMode() throws Exception{
        final boolean supportWifi = mPackageManager.hasSystemFeature(FEATURE_WIFI);
        final boolean supportTelephony = mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
        // store the current state of airplane mode
        final boolean isAirplaneModeEnabled = isAirplaneModeEnabled();
        final TestableNetworkCallback wifiCb = new TestableNetworkCallback();
        final TestableNetworkCallback telephonyCb = new TestableNetworkCallback();
        // disable airplane mode to reach a known state
        runShellCommand("cmd connectivity airplane-mode disable");
        // Verify that networks are available as expected if wifi or cell is supported. Continue the
        // test if none of them are supported since test should still able to verify the permission
        // mechanism.
        if (supportWifi) {
            mCtsNetUtils.ensureWifiConnected();
            registerCallbackAndWaitForAvailable(makeWifiNetworkRequest(), wifiCb);
        }
        if (supportTelephony) {
            // connectToCell needs to be followed by disconnectFromCell, which is called in tearDown
            mCtsNetUtils.connectToCell();
            registerCallbackAndWaitForAvailable(makeCellNetworkRequest(), telephonyCb);
        }

        try {
            // Verify we cannot set Airplane Mode without correct permission:
            try {
                setAndVerifyAirplaneMode(true);
                fail("SecurityException should have been thrown when setAirplaneMode was called"
                        + "without holding permission NETWORK_AIRPLANE_MODE.");
            } catch (SecurityException expected) {}

            // disable airplane mode again to reach a known state
            runShellCommand("cmd connectivity airplane-mode disable");

            // adopt shell permission which holds NETWORK_AIRPLANE_MODE
            mUiAutomation.adoptShellPermissionIdentity();

            // Verify we can enable Airplane Mode with correct permission:
            try {
                setAndVerifyAirplaneMode(true);
            } catch (SecurityException e) {
                fail("SecurityException should not have been thrown when setAirplaneMode(true) was"
                        + "called whilst holding the NETWORK_AIRPLANE_MODE permission.");
            }
            // Verify that the enabling airplane mode takes effect as expected to prevent flakiness
            // caused by fast airplane mode switches. Ensure network lost before turning off
            // airplane mode.
            if (supportWifi) waitForLost(wifiCb);
            if (supportTelephony) waitForLost(telephonyCb);

            // Verify we can disable Airplane Mode with correct permission:
            try {
                setAndVerifyAirplaneMode(false);
            } catch (SecurityException e) {
                fail("SecurityException should not have been thrown when setAirplaneMode(false) was"
                        + "called whilst holding the NETWORK_AIRPLANE_MODE permission.");
            }
            // Verify that turning airplane mode off takes effect as expected.
            // connectToCell only registers a request, it cannot / does not need to be called twice
            mCtsNetUtils.ensureWifiConnected();
            if (supportWifi) waitForAvailable(wifiCb);
            if (supportTelephony) waitForAvailable(telephonyCb);
        } finally {
            // Restore the previous state of airplane mode and permissions:
            runShellCommand("cmd connectivity airplane-mode "
                    + (isAirplaneModeEnabled ? "enable" : "disable"));
            mUiAutomation.dropShellPermissionIdentity();
        }
    }

    private void registerCallbackAndWaitForAvailable(@NonNull final NetworkRequest request,
            @NonNull final TestableNetworkCallback cb) {
        registerNetworkCallback(request, cb);
        waitForAvailable(cb);
    }

    private void waitForAvailable(@NonNull final TestableNetworkCallback cb) {
        cb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
                c -> c instanceof CallbackEntry.Available);
    }

    private void waitForAvailable(
            @NonNull final TestableNetworkCallback cb, final int expectedTransport) {
        cb.eventuallyExpect(
                CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
                entry -> {
                    final NetworkCapabilities nc = mCm.getNetworkCapabilities(entry.getNetwork());
                    return nc.hasTransport(expectedTransport);
                }
        );
    }

    private void waitForAvailable(
            @NonNull final TestableNetworkCallback cb, @NonNull final Network expectedNetwork) {
        cb.expectAvailableCallbacks(expectedNetwork, false /* suspended */,
                null /* validated */,
                false /* blocked */, NETWORK_CALLBACK_TIMEOUT_MS);
    }

    private void waitForLost(@NonNull final TestableNetworkCallback cb) {
        cb.eventuallyExpect(CallbackEntry.LOST, NETWORK_CALLBACK_TIMEOUT_MS,
                c -> c instanceof CallbackEntry.Lost);
    }

    private void setAndVerifyAirplaneMode(Boolean expectedResult)
            throws Exception {
        final CompletableFuture<Boolean> actualResult = new CompletableFuture();
        BroadcastReceiver receiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                // The defaultValue of getExtraBoolean should be the opposite of what is
                // expected, thus ensuring a test failure if the extra is absent.
                actualResult.complete(intent.getBooleanExtra("state", !expectedResult));
            }
        };
        try {
            mContext.registerReceiver(receiver,
                    new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
            mCm.setAirplaneMode(expectedResult);
            final String msg = "Setting Airplane Mode failed,";
            assertEquals(msg, expectedResult, actualResult.get(AIRPLANE_MODE_CHANGE_TIMEOUT_MS,
                    TimeUnit.MILLISECONDS));
        } finally {
            mContext.unregisterReceiver(receiver);
        }
    }

    private static boolean isAirplaneModeEnabled() {
        return runShellCommand("cmd connectivity airplane-mode")
                .trim().equals("enabled");
    }

    @Test
    public void testGetCaptivePortalServerUrl() {
        final String permission = Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q
                ? CONNECTIVITY_INTERNAL
                : NETWORK_SETTINGS;
        final String url = runAsShell(permission, mCm::getCaptivePortalServerUrl);
        assertNotNull("getCaptivePortalServerUrl must not be null", url);
        try {
            final URL parsedUrl = new URL(url);
            // As per the javadoc, the URL must be HTTP
            assertEquals("Invalid captive portal URL protocol", "http", parsedUrl.getProtocol());
        } catch (MalformedURLException e) {
            throw new AssertionFailedError("Captive portal server URL is invalid: " + e);
        }
    }

    /**
     * Verifies that apps are forbidden from getting ssid information from
     * {@Code NetworkCapabilities} if they do not hold NETWORK_SETTINGS permission.
     * See b/161370134.
     */
    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    public void testSsidInNetworkCapabilities() throws Exception {
        assumeTrue("testSsidInNetworkCapabilities cannot execute unless device supports WiFi",
                mPackageManager.hasSystemFeature(FEATURE_WIFI));

        final Network network = mCtsNetUtils.ensureWifiConnected();
        final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
        assertNotNull("Ssid getting from WifiManager is null", ssid);
        // This package should have no NETWORK_SETTINGS permission. Verify that no ssid is contained
        // in the NetworkCapabilities.
        verifySsidFromQueriedNetworkCapabilities(network, ssid, false /* hasSsid */);
        verifySsidFromCallbackNetworkCapabilities(ssid, false /* hasSsid */);
        // Adopt shell permission to allow to get ssid information.
        runWithShellPermissionIdentity(() -> {
            verifySsidFromQueriedNetworkCapabilities(network, ssid, true /* hasSsid */);
            verifySsidFromCallbackNetworkCapabilities(ssid, true /* hasSsid */);
        });
    }

    private void verifySsidFromQueriedNetworkCapabilities(@NonNull Network network,
            @NonNull String ssid, boolean hasSsid) throws Exception {
        // Verify if ssid is contained in NetworkCapabilities queried from ConnectivityManager.
        final NetworkCapabilities nc = mCm.getNetworkCapabilities(network);
        assertNotNull("NetworkCapabilities of the network is null", nc);
        assertEquals(hasSsid, Pattern.compile(ssid).matcher(nc.toString()).find());
    }

    private void verifySsidFromCallbackNetworkCapabilities(@NonNull String ssid, boolean hasSsid)
            throws Exception {
        final CompletableFuture<NetworkCapabilities> foundNc = new CompletableFuture();
        final NetworkCallback callback = new NetworkCallback() {
            @Override
            public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
                foundNc.complete(nc);
            }
        };

        registerNetworkCallback(makeWifiNetworkRequest(), callback);
        // Registering a callback here guarantees onCapabilitiesChanged is called immediately
        // because WiFi network should be connected.
        final NetworkCapabilities nc =
                foundNc.get(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        // Verify if ssid is contained in the NetworkCapabilities received from callback.
        assertNotNull("NetworkCapabilities of the network is null", nc);
        assertEquals(hasSsid, Pattern.compile(ssid).matcher(nc.toString()).find());
    }

    /**
     * Verify background request can only be requested when acquiring
     * {@link android.Manifest.permission.NETWORK_SETTINGS}.
     */
    @AppModeFull(reason = "Instant apps cannot create test networks")
    @Test
    public void testRequestBackgroundNetwork() {
        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
        // shims, and @IgnoreUpTo does not check that.
        assumeTrue(TestUtils.shouldTestSApis());

        // Create a tun interface. Use the returned interface name as the specifier to create
        // a test network request.
        final TestNetworkManager tnm = runWithShellPermissionIdentity(() ->
                mContext.getSystemService(TestNetworkManager.class),
                android.Manifest.permission.MANAGE_TEST_NETWORKS);
        final TestNetworkInterface testNetworkInterface = runWithShellPermissionIdentity(() ->
                    tnm.createTunInterface(new LinkAddress[]{TEST_LINKADDR}),
                android.Manifest.permission.MANAGE_TEST_NETWORKS,
                android.Manifest.permission.NETWORK_SETTINGS);
        assertNotNull(testNetworkInterface);

        final NetworkRequest testRequest = new NetworkRequest.Builder()
                .addTransportType(TRANSPORT_TEST)
                // Test networks do not have NOT_VPN or TRUSTED capabilities by default
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
                .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                        testNetworkInterface.getInterfaceName()))
                .build();

        // Verify background network cannot be requested without NETWORK_SETTINGS permission.
        final TestableNetworkCallback callback = new TestableNetworkCallback();
        final Handler handler = new Handler(Looper.getMainLooper());
        assertThrows(SecurityException.class,
                () -> requestBackgroundNetwork(testRequest, callback, handler));

        Network testNetwork = null;
        try {
            // Request background test network via Shell identity which has NETWORK_SETTINGS
            // permission granted.
            runWithShellPermissionIdentity(
                    () -> requestBackgroundNetwork(testRequest, callback, handler),
                    new String[] { android.Manifest.permission.NETWORK_SETTINGS });

            // Register the test network agent which has no foreground request associated to it.
            // And verify it can satisfy the background network request just fired.
            final Binder binder = new Binder();
            runWithShellPermissionIdentity(() ->
                    tnm.setupTestNetwork(testNetworkInterface.getInterfaceName(), binder),
                    new String[] { android.Manifest.permission.MANAGE_TEST_NETWORKS,
                            android.Manifest.permission.NETWORK_SETTINGS });
            waitForAvailable(callback);
            testNetwork = callback.getLastAvailableNetwork();
            assertNotNull(testNetwork);

            // The test network that has just connected is a foreground network,
            // non-listen requests will get available callback before it can be put into
            // background if no foreground request can be satisfied. Thus, wait for a short
            // period is needed to let foreground capability go away.
            callback.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
                    NETWORK_CALLBACK_TIMEOUT_MS,
                    c -> c instanceof CallbackEntry.CapabilitiesChanged
                            && !((CallbackEntry.CapabilitiesChanged) c).getCaps()
                            .hasCapability(NET_CAPABILITY_FOREGROUND));
            final NetworkCapabilities nc = mCm.getNetworkCapabilities(testNetwork);
            assertFalse("expected background network, but got " + nc,
                    nc.hasCapability(NET_CAPABILITY_FOREGROUND));
        } finally {
            final Network n = testNetwork;
            runWithShellPermissionIdentity(() -> {
                if (null != n) {
                    tnm.teardownTestNetwork(n);
                    callback.eventuallyExpect(CallbackEntry.LOST,
                            NETWORK_CALLBACK_TIMEOUT_MS,
                            lost -> n.equals(lost.getNetwork()));
                }
                testNetworkInterface.getFileDescriptor().close();
            }, new String[] { android.Manifest.permission.MANAGE_TEST_NETWORKS });
        }
    }

    private class DetailedBlockedStatusCallback extends TestableNetworkCallback {
        public void expectAvailableCallbacks(Network network) {
            super.expectAvailableCallbacks(network, false /* suspended */, true /* validated */,
                    BLOCKED_REASON_NONE, NETWORK_CALLBACK_TIMEOUT_MS);
        }
        public void expectBlockedStatusCallback(Network network, int blockedStatus) {
            super.expectBlockedStatusCallback(blockedStatus, network, NETWORK_CALLBACK_TIMEOUT_MS);
        }
        public void onBlockedStatusChanged(Network network, int blockedReasons) {
            getHistory().add(new CallbackEntry.BlockedStatusInt(network, blockedReasons));
        }
        private void assertNoBlockedStatusCallback() {
            super.assertNoCallbackThat(NO_CALLBACK_TIMEOUT_MS,
                    c -> c instanceof CallbackEntry.BlockedStatus);
        }
    }

    private void setRequireVpnForUids(boolean requireVpn, Collection<Range<Integer>> ranges)
            throws Exception {
        mCmShim.setRequireVpnForUids(requireVpn, ranges);
        for (Range<Integer> range : ranges) {
            if (requireVpn) {
                mVpnRequiredUidRanges.add(range);
            } else {
                assertTrue(mVpnRequiredUidRanges.remove(range));
            }
        }
    }

    private void doTestBlockedStatusCallback() throws Exception {
        final DetailedBlockedStatusCallback myUidCallback = new DetailedBlockedStatusCallback();
        final DetailedBlockedStatusCallback otherUidCallback = new DetailedBlockedStatusCallback();

        final int myUid = Process.myUid();
        final int otherUid = UserHandle.getUid(5, Process.FIRST_APPLICATION_UID);
        final Handler handler = new Handler(Looper.getMainLooper());

        registerDefaultNetworkCallback(myUidCallback, handler);
        registerDefaultNetworkCallbackForUid(otherUid, otherUidCallback, handler);

        final Network defaultNetwork = mCm.getActiveNetwork();
        final List<DetailedBlockedStatusCallback> allCallbacks =
                List.of(myUidCallback, otherUidCallback);
        for (DetailedBlockedStatusCallback callback : allCallbacks) {
            callback.expectAvailableCallbacks(defaultNetwork);
        }

        final Range<Integer> myUidRange = new Range<>(myUid, myUid);
        final Range<Integer> otherUidRange = new Range<>(otherUid, otherUid);

        setRequireVpnForUids(true, List.of(myUidRange));
        myUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_LOCKDOWN_VPN);
        otherUidCallback.assertNoBlockedStatusCallback();

        setRequireVpnForUids(true, List.of(myUidRange, otherUidRange));
        myUidCallback.assertNoBlockedStatusCallback();
        otherUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_LOCKDOWN_VPN);

        // setRequireVpnForUids does no deduplication or refcounting. Removing myUidRange does not
        // unblock myUid because it was added to the blocked ranges twice.
        setRequireVpnForUids(false, List.of(myUidRange));
        myUidCallback.assertNoBlockedStatusCallback();
        otherUidCallback.assertNoBlockedStatusCallback();

        setRequireVpnForUids(false, List.of(myUidRange, otherUidRange));
        myUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_NONE);
        otherUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_NONE);

        myUidCallback.assertNoBlockedStatusCallback();
        otherUidCallback.assertNoBlockedStatusCallback();
    }

    @Test
    public void testBlockedStatusCallback() {
        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
        // shims, and @IgnoreUpTo does not check that.
        assumeTrue(TestUtils.shouldTestSApis());
        runWithShellPermissionIdentity(() -> doTestBlockedStatusCallback(), NETWORK_SETTINGS);
    }

    private void doTestLegacyLockdownEnabled() throws Exception {
        NetworkInfo info = mCm.getActiveNetworkInfo();
        assertNotNull(info);
        assertEquals(DetailedState.CONNECTED, info.getDetailedState());

        final TestableNetworkCallback callback = new TestableNetworkCallback();
        try {
            mCmShim.setLegacyLockdownVpnEnabled(true);

            // setLegacyLockdownVpnEnabled is asynchronous and only takes effect when the
            // ConnectivityService handler thread processes it. Ensure it has taken effect by doing
            // something that blocks until the handler thread is idle.
            registerDefaultNetworkCallback(callback);
            waitForAvailable(callback);

            // Test one of the effects of setLegacyLockdownVpnEnabled: the fact that any NetworkInfo
            // in state CONNECTED is degraded to CONNECTING if the legacy VPN is not connected.
            info = mCm.getActiveNetworkInfo();
            assertNotNull(info);
            assertEquals(DetailedState.CONNECTING, info.getDetailedState());
        } finally {
            mCmShim.setLegacyLockdownVpnEnabled(false);
        }
    }

    @Test
    public void testLegacyLockdownEnabled() {
        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
        // shims, and @IgnoreUpTo does not check that.
        assumeTrue(TestUtils.shouldTestSApis());
        runWithShellPermissionIdentity(() -> doTestLegacyLockdownEnabled(), NETWORK_SETTINGS);
    }

    @Test
    public void testGetCapabilityCarrierName() {
        assumeTrue(TestUtils.shouldTestSApis());
        assertEquals("ENTERPRISE", NetworkInformationShimImpl.newInstance()
                .getCapabilityCarrierName(ConstantsShim.NET_CAPABILITY_ENTERPRISE));
        assertNull(NetworkInformationShimImpl.newInstance()
                .getCapabilityCarrierName(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
    }

    @Test
    public void testSetGlobalProxy() {
        assumeTrue(TestUtils.shouldTestSApis());
        // Behavior is verified in gts. Verify exception thrown w/o permission.
        assertThrows(SecurityException.class, () -> mCm.setGlobalProxy(
                ProxyInfo.buildDirectProxy("example.com" /* host */, 8080 /* port */)));
    }

    @Test
    public void testFactoryResetWithoutPermission() {
        assumeTrue(TestUtils.shouldTestSApis());
        assertThrows(SecurityException.class, () -> mCm.factoryReset());
    }

    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    public void testFactoryReset() throws Exception {
        assumeTrue(TestUtils.shouldTestSApis());

        // Store current settings.
        final int curAvoidBadWifi =
                ConnectivitySettingsManager.getNetworkAvoidBadWifi(mContext);
        final int curPrivateDnsMode = ConnectivitySettingsManager.getPrivateDnsMode(mContext);

        TestTetheringEventCallback tetherEventCallback = null;
        final CtsTetheringUtils tetherUtils = new CtsTetheringUtils(mContext);
        try {
            tetherEventCallback = tetherUtils.registerTetheringEventCallback();
            // Adopt for NETWORK_SETTINGS permission.
            mUiAutomation.adoptShellPermissionIdentity();
            // start tethering
            tetherEventCallback.assumeWifiTetheringSupported(mContext);
            tetherUtils.startWifiTethering(tetherEventCallback);
            // Update setting to verify the behavior.
            mCm.setAirplaneMode(true);
            ConnectivitySettingsManager.setPrivateDnsMode(mContext,
                    ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF);
            ConnectivitySettingsManager.setNetworkAvoidBadWifi(mContext,
                    ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI_IGNORE);
            assertEquals(AIRPLANE_MODE_ON, Settings.Global.getInt(
                    mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON));
            // Verify factoryReset
            mCm.factoryReset();
            verifySettings(AIRPLANE_MODE_OFF,
                    ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC,
                    ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI_PROMPT);

            tetherEventCallback.expectNoTetheringActive();
        } finally {
            // Restore settings.
            mCm.setAirplaneMode(false);
            ConnectivitySettingsManager.setNetworkAvoidBadWifi(mContext, curAvoidBadWifi);
            ConnectivitySettingsManager.setPrivateDnsMode(mContext, curPrivateDnsMode);
            if (tetherEventCallback != null) {
                tetherUtils.unregisterTetheringEventCallback(tetherEventCallback);
            }
            tetherUtils.stopAllTethering();
            mUiAutomation.dropShellPermissionIdentity();
        }
    }

    /**
     * Verify that {@link ConnectivityManager#setProfileNetworkPreference} cannot be called
     * without required NETWORK_STACK permissions.
     */
    @Test
    public void testSetProfileNetworkPreference_NoPermission() {
        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
        // shims, and @IgnoreUpTo does not check that.
        assumeTrue(TestUtils.shouldTestSApis());
        assertThrows(SecurityException.class, () -> mCm.setProfileNetworkPreference(
                UserHandle.of(0), PROFILE_NETWORK_PREFERENCE_ENTERPRISE, null /* executor */,
                null /* listener */));
    }

    @Test
    public void testSystemReady() {
        assumeTrue(TestUtils.shouldTestSApis());
        assertThrows(SecurityException.class, () -> mCm.systemReady());
    }

    @Test
    public void testGetIpSecNetIdRange() {
        assumeTrue(TestUtils.shouldTestSApis());
        // The lower refers to ConnectivityManager.TUN_INTF_NETID_START.
        final long lower = 64512;
        // The upper refers to ConnectivityManager.TUN_INTF_NETID_START
        // + ConnectivityManager.TUN_INTF_NETID_RANGE - 1
        final long upper = 65535;
        assertEquals(lower, (long) ConnectivityManager.getIpSecNetIdRange().getLower());
        assertEquals(upper, (long) ConnectivityManager.getIpSecNetIdRange().getUpper());
    }

    private void verifySettings(int expectedAirplaneMode, int expectedPrivateDnsMode,
            int expectedAvoidBadWifi) throws Exception {
        assertEquals(expectedAirplaneMode, Settings.Global.getInt(
                mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON));
        assertEquals(expectedPrivateDnsMode,
                ConnectivitySettingsManager.getPrivateDnsMode(mContext));
        assertEquals(expectedAvoidBadWifi,
                ConnectivitySettingsManager.getNetworkAvoidBadWifi(mContext));
    }

    /**
     * Verify that per-app OEM network preference functions as expected for network preference TEST.
     * For specified apps, validate networks are prioritized in order: unmetered, TEST transport,
     * default network.
     */
    @AppModeFull(reason = "Instant apps cannot create test networks")
    @Test
    public void testSetOemNetworkPreferenceForTestPref() throws Exception {
        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
        // shims, and @IgnoreUpTo does not check that.
        assumeTrue(TestUtils.shouldTestSApis());
        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));

        final TestNetworkTracker tnt = callWithShellPermissionIdentity(
                () -> initTestNetwork(mContext, TEST_LINKADDR, NETWORK_CALLBACK_TIMEOUT_MS));
        final TestableNetworkCallback defaultCallback = new TestableNetworkCallback();
        final TestableNetworkCallback systemDefaultCallback = new TestableNetworkCallback();

        final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
        final NetworkCapabilities wifiNetworkCapabilities = callWithShellPermissionIdentity(
                () -> mCm.getNetworkCapabilities(wifiNetwork));
        final String ssid = unquoteSSID(wifiNetworkCapabilities.getSsid());
        final boolean oldMeteredValue = wifiNetworkCapabilities.isMetered();

        testAndCleanup(() -> {
            // This network will be used for unmetered. Wait for it to be validated because
            // OEM_NETWORK_PREFERENCE_TEST only prefers NOT_METERED&VALIDATED to a network with
            // TRANSPORT_TEST, like OEM_NETWORK_PREFERENCE_OEM_PAID.
            setWifiMeteredStatusAndWait(ssid, false /* isMetered */, true /* waitForValidation */);

            setOemNetworkPreferenceForMyPackage(OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST);
            registerTestOemNetworkPreferenceCallbacks(defaultCallback, systemDefaultCallback);

            // Validate that an unmetered network is used over other networks.
            waitForAvailable(defaultCallback, wifiNetwork);
            waitForAvailable(systemDefaultCallback, wifiNetwork);

            // Validate that when setting unmetered to metered, unmetered is lost and replaced by
            // the network with the TEST transport. Also wait for validation here, in case there
            // is a bug that's only visible when the network is validated.
            setWifiMeteredStatusAndWait(ssid, true /* isMetered */, true /* waitForValidation */);
            defaultCallback.expectCallback(CallbackEntry.LOST, wifiNetwork,
                    NETWORK_CALLBACK_TIMEOUT_MS);
            waitForAvailable(defaultCallback, tnt.getNetwork());
            // Depending on if this device has cellular connectivity or not, multiple available
            // callbacks may be received. Eventually, metered Wi-Fi should be the final available
            // callback in any case therefore confirm its receipt before continuing to assure the
            // system is in the expected state.
            waitForAvailable(systemDefaultCallback, TRANSPORT_WIFI);
        }, /* cleanup */ () -> {
            // Validate that removing the test network will fallback to the default network.
            runWithShellPermissionIdentity(tnt::teardown);
            defaultCallback.expectCallback(CallbackEntry.LOST, tnt.getNetwork(),
                    NETWORK_CALLBACK_TIMEOUT_MS);
            waitForAvailable(defaultCallback);
            }, /* cleanup */ () -> {
                setWifiMeteredStatusAndWait(ssid, oldMeteredValue, false /* waitForValidation */);
            }, /* cleanup */ () -> {
                // Cleanup any prior test state from setOemNetworkPreference
                clearOemNetworkPreference();
            });
    }

    /**
     * Verify that per-app OEM network preference functions as expected for network pref TEST_ONLY.
     * For specified apps, validate that only TEST transport type networks are used.
     */
    @AppModeFull(reason = "Instant apps cannot create test networks")
    @Test
    public void testSetOemNetworkPreferenceForTestOnlyPref() throws Exception {
        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
        // shims, and @IgnoreUpTo does not check that.
        assumeTrue(TestUtils.shouldTestSApis());

        final TestNetworkTracker tnt = callWithShellPermissionIdentity(
                () -> initTestNetwork(mContext, TEST_LINKADDR, NETWORK_CALLBACK_TIMEOUT_MS));
        final TestableNetworkCallback defaultCallback = new TestableNetworkCallback();
        final TestableNetworkCallback systemDefaultCallback = new TestableNetworkCallback();

        final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();

        testAndCleanup(() -> {
            setOemNetworkPreferenceForMyPackage(
                    OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST_ONLY);
            registerTestOemNetworkPreferenceCallbacks(defaultCallback, systemDefaultCallback);
            waitForAvailable(defaultCallback, tnt.getNetwork());
            waitForAvailable(systemDefaultCallback, wifiNetwork);
        }, /* cleanup */ () -> {
                runWithShellPermissionIdentity(tnt::teardown);
                defaultCallback.expectCallback(CallbackEntry.LOST, tnt.getNetwork(),
                        NETWORK_CALLBACK_TIMEOUT_MS);

                // This network preference should only ever use the test network therefore available
                // should not trigger when the test network goes down (e.g. switch to cellular).
                defaultCallback.assertNoCallback();
                // The system default should still be connected to Wi-fi
                assertEquals(wifiNetwork, systemDefaultCallback.getLastAvailableNetwork());
            }, /* cleanup */ () -> {
                // Cleanup any prior test state from setOemNetworkPreference
                clearOemNetworkPreference();

                // The default (non-test) network should be available as the network pref was
                // cleared.
                waitForAvailable(defaultCallback);
            });
    }

    private void registerTestOemNetworkPreferenceCallbacks(
            @NonNull final TestableNetworkCallback defaultCallback,
            @NonNull final TestableNetworkCallback systemDefaultCallback) {
        registerDefaultNetworkCallback(defaultCallback);
        runWithShellPermissionIdentity(() ->
                registerSystemDefaultNetworkCallback(systemDefaultCallback,
                        new Handler(Looper.getMainLooper())), NETWORK_SETTINGS);
    }

    private static final class OnCompleteListenerCallback {
        final CompletableFuture<Object> mDone = new CompletableFuture<>();

        public void onComplete() {
            mDone.complete(new Object());
        }

        void expectOnComplete() throws Exception {
            try {
                mDone.get(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
            } catch (TimeoutException e) {
                fail("Expected onComplete() not received after "
                        + NETWORK_CALLBACK_TIMEOUT_MS + " ms");
            }
        }
    }

    private void setOemNetworkPreferenceForMyPackage(final int networkPref) throws Exception {
        final OemNetworkPreferences pref = new OemNetworkPreferences.Builder()
                .addNetworkPreference(mContext.getPackageName(), networkPref)
                .build();
        final OnCompleteListenerCallback oemPrefListener = new OnCompleteListenerCallback();
        mUiAutomation.adoptShellPermissionIdentity();
        try {
            mCm.setOemNetworkPreference(
                    pref, mContext.getMainExecutor(), oemPrefListener::onComplete);
        } finally {
            mUiAutomation.dropShellPermissionIdentity();
        }
        oemPrefListener.expectOnComplete();
    }

    /**
     * This will clear the OEM network preference on the device. As there is currently no way of
     * getting the existing preference, if this is executed while an existing preference is in
     * place, that preference will need to be reapplied after executing this test.
     * @throws Exception
     */
    private void clearOemNetworkPreference() throws Exception {
        final OemNetworkPreferences clearPref = new OemNetworkPreferences.Builder().build();
        final OnCompleteListenerCallback oemPrefListener = new OnCompleteListenerCallback();
        mUiAutomation.adoptShellPermissionIdentity();
        try {
            mCm.setOemNetworkPreference(
                    clearPref, mContext.getMainExecutor(), oemPrefListener::onComplete);
        } finally {
            mUiAutomation.dropShellPermissionIdentity();
        }
        oemPrefListener.expectOnComplete();
    }

    @Test
    public void testSetAcceptPartialConnectivity_NoPermission_GetException() {
        assumeTrue(TestUtils.shouldTestSApis());
        assertThrows(SecurityException.class, () -> mCm.setAcceptPartialConnectivity(
                mCm.getActiveNetwork(), false /* accept */ , false /* always */));
    }

    @AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
    @Test
    public void testAcceptPartialConnectivity_validatedNetwork() throws Exception {
        assumeTrue(TestUtils.shouldTestSApis());
        assumeTrue("testAcceptPartialConnectivity_validatedNetwork cannot execute"
                        + " unless device supports WiFi",
                mPackageManager.hasSystemFeature(FEATURE_WIFI));

        try {
            // Wait for partial connectivity to be detected on the network
            final Network network = preparePartialConnectivity();

            runAsShell(NETWORK_SETTINGS, () -> {
                // The always bit is verified in NetworkAgentTest
                mCm.setAcceptPartialConnectivity(network, true /* accept */, false /* always */);
            });

            // Accept partial connectivity network should result in a validated network
            expectNetworkHasCapability(network, NET_CAPABILITY_VALIDATED, WIFI_CONNECT_TIMEOUT_MS);
        } finally {
            mHttpServer.stop();
            mTestValidationConfigRule.runAfterNextCleanup(this::reconnectWifi);
        }
    }

    @AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
    @Test
    public void testRejectPartialConnectivity_TearDownNetwork() throws Exception {
        assumeTrue(TestUtils.shouldTestSApis());
        assumeTrue("testAcceptPartialConnectivity_validatedNetwork cannot execute"
                        + " unless device supports WiFi",
                mPackageManager.hasSystemFeature(FEATURE_WIFI));

        final TestNetworkCallback cb = new TestNetworkCallback();
        try {
            // Wait for partial connectivity to be detected on the network
            final Network network = preparePartialConnectivity();

            requestNetwork(makeWifiNetworkRequest(), cb);
            runAsShell(NETWORK_SETTINGS, () -> {
                // The always bit is verified in NetworkAgentTest
                mCm.setAcceptPartialConnectivity(network, false /* accept */, false /* always */);
            });
            // Reject partial connectivity network should cause the network being torn down
            assertEquals(network, cb.waitForLost());
        } finally {
            mHttpServer.stop();
            // Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
            // apply here. Thus, turn off wifi first and restart to restore.
            mTestValidationConfigRule.runAfterNextCleanup(() -> {
                runShellCommand("svc wifi disable");
                mCtsNetUtils.ensureWifiConnected();
            });
        }
    }

    @Test
    public void testSetAcceptUnvalidated_NoPermission_GetException() {
        assumeTrue(TestUtils.shouldTestSApis());
        assertThrows(SecurityException.class, () -> mCm.setAcceptUnvalidated(
                mCm.getActiveNetwork(), false /* accept */ , false /* always */));
    }

    @AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
    @Test
    public void testRejectUnvalidated_TearDownNetwork() throws Exception {
        assumeTrue(TestUtils.shouldTestSApis());
        final boolean canRunTest = mPackageManager.hasSystemFeature(FEATURE_WIFI)
                && mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
        assumeTrue("testAcceptPartialConnectivity_validatedNetwork cannot execute"
                        + " unless device supports WiFi and telephony", canRunTest);

        final TestableNetworkCallback wifiCb = new TestableNetworkCallback();
        try {
            // Ensure at least one default network candidate connected.
            mCtsNetUtils.connectToCell();

            final Network wifiNetwork = prepareUnvalidatedNetwork();
            // Default network should not be wifi ,but checking that wifi is not the default doesn't
            // guarantee that it won't become the default in the future.
            assertNotEquals(wifiNetwork, mCm.getActiveNetwork());

            registerNetworkCallback(makeWifiNetworkRequest(), wifiCb);
            runAsShell(NETWORK_SETTINGS, () -> {
                mCm.setAcceptUnvalidated(wifiNetwork, false /* accept */, false /* always */);
            });
            waitForLost(wifiCb);
        } finally {
            mHttpServer.stop();
            /// Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
            // apply here. Thus, turn off wifi first and restart to restore.
            mTestValidationConfigRule.runAfterNextCleanup(() -> {
                runShellCommand("svc wifi disable");
                mCtsNetUtils.ensureWifiConnected();
            });
        }
    }

    @AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
    @Test
    public void testSetAvoidUnvalidated() throws Exception {
        assumeTrue(TestUtils.shouldTestSApis());
        // TODO: Allow in debuggable ROM only. To be replaced by FabricatedOverlay
        assumeTrue(Build.isDebuggable());
        final boolean canRunTest = mPackageManager.hasSystemFeature(FEATURE_WIFI)
                && mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
        assumeTrue("testSetAvoidUnvalidated cannot execute"
                + " unless device supports WiFi and telephony", canRunTest);

        final TestableNetworkCallback wifiCb = new TestableNetworkCallback();
        final TestableNetworkCallback defaultCb = new TestableNetworkCallback();
        final int previousAvoidBadWifi =
                ConnectivitySettingsManager.getNetworkAvoidBadWifi(mContext);

        allowBadWifi();

        final Network cellNetwork = mCtsNetUtils.connectToCell();
        final Network wifiNetwork = prepareValidatedNetwork();

        registerDefaultNetworkCallback(defaultCb);
        registerNetworkCallback(makeWifiNetworkRequest(), wifiCb);

        try {
            // Verify wifi is the default network.
            defaultCb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
                    entry -> wifiNetwork.equals(entry.getNetwork()));
            wifiCb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
                    entry -> wifiNetwork.equals(entry.getNetwork()));
            assertTrue(mCm.getNetworkCapabilities(wifiNetwork).hasCapability(
                    NET_CAPABILITY_VALIDATED));

            // Configure response code for unvalidated network
            configTestServer(Status.INTERNAL_ERROR, Status.INTERNAL_ERROR);
            mCm.reportNetworkConnectivity(wifiNetwork, false);
            // Default network should stay on unvalidated wifi because avoid bad wifi is disabled.
            defaultCb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
                    NETWORK_CALLBACK_TIMEOUT_MS,
                    entry -> !((CallbackEntry.CapabilitiesChanged) entry).getCaps()
                            .hasCapability(NET_CAPABILITY_VALIDATED));
            wifiCb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
                    NETWORK_CALLBACK_TIMEOUT_MS,
                    entry -> !((CallbackEntry.CapabilitiesChanged) entry).getCaps()
                            .hasCapability(NET_CAPABILITY_VALIDATED));

            runAsShell(NETWORK_SETTINGS, () -> {
                mCm.setAvoidUnvalidated(wifiNetwork);
            });
            // Default network should be updated to validated cellular network.
            defaultCb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
                    entry -> cellNetwork.equals(entry.getNetwork()));
            // The network should not validate again.
            wifiCb.assertNoCallbackThat(NO_CALLBACK_TIMEOUT_MS, c -> isValidatedCaps(c));
        } finally {
            resetAvoidBadWifi(previousAvoidBadWifi);
            mHttpServer.stop();
            mTestValidationConfigRule.runAfterNextCleanup(this::reconnectWifi);
        }
    }

    private boolean isValidatedCaps(CallbackEntry c) {
        if (!(c instanceof CallbackEntry.CapabilitiesChanged)) return false;
        final CallbackEntry.CapabilitiesChanged capsChanged = (CallbackEntry.CapabilitiesChanged) c;
        return capsChanged.getCaps().hasCapability(NET_CAPABILITY_VALIDATED);
    }

    private void resetAvoidBadWifi(int settingValue) {
        setTestAllowBadWifiResource(0 /* timeMs */);
        ConnectivitySettingsManager.setNetworkAvoidBadWifi(mContext, settingValue);
    }

    private void allowBadWifi() {
        setTestAllowBadWifiResource(
                System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS /* timeMs */);
        ConnectivitySettingsManager.setNetworkAvoidBadWifi(mContext,
                ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI_IGNORE);
    }

    private void setTestAllowBadWifiResource(long timeMs) {
        runAsShell(NETWORK_SETTINGS, () -> {
            mCm.setTestAllowBadWifiUntil(timeMs);
        });
    }

    private Network expectNetworkHasCapability(Network network, int expectedNetCap, long timeout)
            throws Exception {
        final CompletableFuture<Network> future = new CompletableFuture();
        final NetworkCallback cb = new NetworkCallback() {
            @Override
            public void onCapabilitiesChanged(Network n, NetworkCapabilities nc) {
                if (n.equals(network) && nc.hasCapability(expectedNetCap)) {
                    future.complete(network);
                }
            }
        };

        registerNetworkCallback(new NetworkRequest.Builder().build(), cb);
        return future.get(timeout, TimeUnit.MILLISECONDS);
    }

    private void prepareHttpServer() throws Exception {
        runAsShell(READ_DEVICE_CONFIG, () -> {
            // Verify that the test URLs are not normally set on the device, but do not fail if the
            // test URLs are set to what this test uses (URLs on localhost), in case the test was
            // interrupted manually and rerun.
            assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL);
            assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL);
        });

        NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig();

        mHttpServer.start();
    }

    private Network reconnectWifi() {
        mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
        return mCtsNetUtils.ensureWifiConnected();
    }

    private Network prepareValidatedNetwork() throws Exception {
        prepareHttpServer();
        configTestServer(Status.NO_CONTENT, Status.NO_CONTENT);
        // Disconnect wifi first then start wifi network with configuration.
        final Network wifiNetwork = reconnectWifi();

        return expectNetworkHasCapability(wifiNetwork, NET_CAPABILITY_VALIDATED,
                WIFI_CONNECT_TIMEOUT_MS);
    }

    private Network preparePartialConnectivity() throws Exception {
        prepareHttpServer();
        // Configure response code for partial connectivity
        configTestServer(Status.INTERNAL_ERROR  /* httpsStatusCode */,
                Status.NO_CONTENT  /* httpStatusCode */);
        // Disconnect wifi first then start wifi network with configuration.
        mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
        final Network network = mCtsNetUtils.ensureWifiConnected();

        return expectNetworkHasCapability(network, NET_CAPABILITY_PARTIAL_CONNECTIVITY,
                WIFI_CONNECT_TIMEOUT_MS);
    }

    private Network prepareUnvalidatedNetwork() throws Exception {
        prepareHttpServer();
        // Configure response code for unvalidated network
        configTestServer(Status.INTERNAL_ERROR /* httpsStatusCode */,
                Status.INTERNAL_ERROR /* httpStatusCode */);

        // Disconnect wifi first then start wifi network with configuration.
        mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
        final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
        return expectNetworkHasCapability(wifiNetwork, NET_CAPABILITY_INTERNET,
                WIFI_CONNECT_TIMEOUT_MS);
    }

    private String makeUrl(String path) {
        return "http://localhost:" + mHttpServer.getListeningPort() + path;
    }

    private void assertEmptyOrLocalhostUrl(String urlKey) {
        final String url = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, urlKey);
        assertTrue(urlKey + " must not be set in production scenarios, current value= " + url,
                TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME.equals(Uri.parse(url).getHost()));
    }

    private void configTestServer(IStatus httpsStatusCode, IStatus httpStatusCode) {
        mHttpServer.addResponse(new TestHttpServer.Request(
                TEST_HTTPS_URL_PATH, Method.GET, "" /* queryParameters */),
                httpsStatusCode, null /* locationHeader */, "" /* content */);
        mHttpServer.addResponse(new TestHttpServer.Request(
                TEST_HTTP_URL_PATH, Method.GET, "" /* queryParameters */),
                httpStatusCode, null /* locationHeader */, "" /* content */);
        NetworkValidationTestUtil.setHttpsUrlDeviceConfig(mTestValidationConfigRule,
                makeUrl(TEST_HTTPS_URL_PATH));
        NetworkValidationTestUtil.setHttpUrlDeviceConfig(mTestValidationConfigRule,
                makeUrl(TEST_HTTP_URL_PATH));
        NetworkValidationTestUtil.setUrlExpirationDeviceConfig(mTestValidationConfigRule,
                System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS);
    }

    @AppModeFull(reason = "Need WiFi support to test the default active network")
    @Test
    public void testDefaultNetworkActiveListener() throws Exception {
        final boolean supportWifi = mPackageManager.hasSystemFeature(FEATURE_WIFI);
        final boolean supportTelephony = mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
        assumeTrue("testDefaultNetworkActiveListener cannot execute"
                + " unless device supports WiFi or telephony", (supportWifi || supportTelephony));

        if (supportWifi) {
            mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
        } else {
            mCtsNetUtils.disconnectFromCell();
        }

        final CompletableFuture<Boolean> future = new CompletableFuture<>();
        final ConnectivityManager.OnNetworkActiveListener listener = () -> future.complete(true);
        mCm.addDefaultNetworkActiveListener(listener);
        testAndCleanup(() -> {
            // New default network connected will trigger a network activity notification.
            if (supportWifi) {
                mCtsNetUtils.ensureWifiConnected();
            } else {
                mCtsNetUtils.connectToCell();
            }
            assertTrue(future.get(LISTEN_ACTIVITY_TIMEOUT_MS, TimeUnit.MILLISECONDS));
        }, () -> {
                mCm.removeDefaultNetworkActiveListener(listener);
            });
    }

    /**
     *  The networks used in this test are real networks and as such they can see seemingly random
     *  updates of their capabilities or link properties as conditions change, e.g. the network
     *  loses validation or IPv4 shows up. Many tests should simply treat these callbacks as
     *  spurious.
     */
    private void assertNoCallbackExceptCapOrLpChange(
            @NonNull final TestableNetworkCallback cb) {
        cb.assertNoCallbackThat(NO_CALLBACK_TIMEOUT_MS,
                c -> !(c instanceof CallbackEntry.CapabilitiesChanged
                        || c instanceof CallbackEntry.LinkPropertiesChanged));
    }

    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
    @Test
    public void testMobileDataPreferredUids() throws Exception {
        assumeTrue(TestUtils.shouldTestSApis());
        final boolean canRunTest = mPackageManager.hasSystemFeature(FEATURE_WIFI)
                && mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
        assumeTrue("testMobileDataPreferredUidsWithCallback cannot execute"
                + " unless device supports both WiFi and telephony", canRunTest);

        final int uid = mPackageManager.getPackageUid(mContext.getPackageName(), 0 /* flag */);
        final Set<Integer> mobileDataPreferredUids =
                ConnectivitySettingsManager.getMobileDataPreferredUids(mContext);
        // CtsNetTestCases uid should not list in MOBILE_DATA_PREFERRED_UIDS setting because it just
        // installs to device. In case the uid is existed in setting mistakenly, try to remove the
        // uid and set correct uids to setting.
        mobileDataPreferredUids.remove(uid);
        ConnectivitySettingsManager.setMobileDataPreferredUids(mContext, mobileDataPreferredUids);

        // For testing mobile data preferred uids feature, it needs both wifi and cell network.
        final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
        final Network cellNetwork = mCtsNetUtils.connectToCell();
        final TestableNetworkCallback defaultTrackingCb = new TestableNetworkCallback();
        final TestableNetworkCallback systemDefaultCb = new TestableNetworkCallback();
        final Handler h = new Handler(Looper.getMainLooper());
        runWithShellPermissionIdentity(() -> registerSystemDefaultNetworkCallback(
                systemDefaultCb, h), NETWORK_SETTINGS);
        registerDefaultNetworkCallback(defaultTrackingCb);

        try {
            // CtsNetTestCases uid is not listed in MOBILE_DATA_PREFERRED_UIDS setting, so the
            // per-app default network should be same as system default network.
            waitForAvailable(systemDefaultCb, wifiNetwork);
            waitForAvailable(defaultTrackingCb, wifiNetwork);
            // Active network for CtsNetTestCases uid should be wifi now.
            assertEquals(wifiNetwork, mCm.getActiveNetwork());

            // Add CtsNetTestCases uid to MOBILE_DATA_PREFERRED_UIDS setting, then available per-app
            // default network callback should be received with cell network.
            final Set<Integer> newMobileDataPreferredUids = new ArraySet<>(mobileDataPreferredUids);
            newMobileDataPreferredUids.add(uid);
            ConnectivitySettingsManager.setMobileDataPreferredUids(
                    mContext, newMobileDataPreferredUids);
            waitForAvailable(defaultTrackingCb, cellNetwork);
            // No change for system default network. Expect no callback except CapabilitiesChanged
            // or LinkPropertiesChanged which may be triggered randomly from wifi network.
            assertNoCallbackExceptCapOrLpChange(systemDefaultCb);
            // Active network for CtsNetTestCases uid should change to cell, too.
            assertEquals(cellNetwork, mCm.getActiveNetwork());

            // Remove CtsNetTestCases uid from MOBILE_DATA_PREFERRED_UIDS setting, then available
            // per-app default network callback should be received again with system default network
            newMobileDataPreferredUids.remove(uid);
            ConnectivitySettingsManager.setMobileDataPreferredUids(
                    mContext, newMobileDataPreferredUids);
            waitForAvailable(defaultTrackingCb, wifiNetwork);
            // No change for system default network. Expect no callback except CapabilitiesChanged
            // or LinkPropertiesChanged which may be triggered randomly from wifi network.
            assertNoCallbackExceptCapOrLpChange(systemDefaultCb);
            // Active network for CtsNetTestCases uid should change back to wifi.
            assertEquals(wifiNetwork, mCm.getActiveNetwork());
        } finally {
            // Restore setting.
            ConnectivitySettingsManager.setMobileDataPreferredUids(
                    mContext, mobileDataPreferredUids);
        }
    }

    private void assertBindSocketToNetworkSuccess(final Network network) throws Exception {
        final CompletableFuture<Boolean> future = new CompletableFuture<>();
        final ExecutorService executor = Executors.newSingleThreadExecutor();
        try {
            executor.execute(() -> {
                for (int i = 0; i < 300; i++) {
                    SystemClock.sleep(10);

                    try (Socket socket = new Socket()) {
                        network.bindSocket(socket);
                        future.complete(true);
                        return;
                    } catch (IOException e) { }
                }
            });
            assertTrue(future.get(APPLYING_UIDS_ALLOWED_ON_RESTRICTED_NETWORKS_TIMEOUT_MS,
                    TimeUnit.MILLISECONDS));
        } finally {
            executor.shutdown();
        }
    }

    private static NetworkAgent createRestrictedNetworkAgent(final Context context) {
        // Create test network agent with restricted network.
        final NetworkCapabilities nc = new NetworkCapabilities.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
                .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                        TEST_RESTRICTED_NW_IFACE_NAME))
                .build();
        final NetworkAgent agent = new NetworkAgent(context, Looper.getMainLooper(), TAG, nc,
                new LinkProperties(), 10 /* score */, new NetworkAgentConfig.Builder().build(),
                new NetworkProvider(context, Looper.getMainLooper(), TAG)) {};
        runWithShellPermissionIdentity(() -> agent.register(),
                android.Manifest.permission.MANAGE_TEST_NETWORKS);
        agent.markConnected();

        return agent;
    }

    @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
    @Test
    public void testUidsAllowedOnRestrictedNetworks() throws Exception {
        assumeTestSApis();

        // TODO (b/175199465): figure out a reasonable permission check for
        //  setUidsAllowedOnRestrictedNetworks that allows tests but not system-external callers.
        assumeTrue(Build.isDebuggable());

        final int uid = mPackageManager.getPackageUid(mContext.getPackageName(), 0 /* flag */);
        final Set<Integer> originalUidsAllowedOnRestrictedNetworks =
                ConnectivitySettingsManager.getUidsAllowedOnRestrictedNetworks(mContext);
        // CtsNetTestCases uid should not list in UIDS_ALLOWED_ON_RESTRICTED_NETWORKS setting
        // because it has been just installed to device. In case the uid is existed in setting
        // mistakenly, try to remove the uid and set correct uids to setting.
        originalUidsAllowedOnRestrictedNetworks.remove(uid);
        runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
                mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);

        // File a restricted network request with permission first to hold the connection.
        final TestableNetworkCallback testNetworkCb = new TestableNetworkCallback();
        final NetworkRequest testRequest = new NetworkRequest.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
                .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                        TEST_RESTRICTED_NW_IFACE_NAME))
                .build();
        runWithShellPermissionIdentity(() -> requestNetwork(testRequest, testNetworkCb),
                CONNECTIVITY_USE_RESTRICTED_NETWORKS);

        // File another restricted network request without permission.
        final TestableNetworkCallback restrictedNetworkCb = new TestableNetworkCallback();
        final NetworkRequest restrictedRequest = new NetworkRequest.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
                .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                        TEST_RESTRICTED_NW_IFACE_NAME))
                .build();
        // Uid is not in allowed list and no permissions. Expect that SecurityException will throw.
        assertThrows(SecurityException.class,
                () -> mCm.requestNetwork(restrictedRequest, restrictedNetworkCb));

        final NetworkAgent agent = createRestrictedNetworkAgent(mContext);
        final Network network = agent.getNetwork();

        try (Socket socket = new Socket()) {
            // Verify that the network is restricted.
            testNetworkCb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
                    NETWORK_CALLBACK_TIMEOUT_MS,
                    entry -> network.equals(entry.getNetwork())
                            && (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
                            .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)));
            // CtsNetTestCases package doesn't hold CONNECTIVITY_USE_RESTRICTED_NETWORKS, so it
            // does not allow to bind socket to restricted network.
            assertThrows(IOException.class, () -> network.bindSocket(socket));

            // Add CtsNetTestCases uid to UIDS_ALLOWED_ON_RESTRICTED_NETWORKS setting, then it can
            // bind socket to restricted network normally.
            final Set<Integer> newUidsAllowedOnRestrictedNetworks =
                    new ArraySet<>(originalUidsAllowedOnRestrictedNetworks);
            newUidsAllowedOnRestrictedNetworks.add(uid);
            runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
                    mContext, newUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
            // Wait a while for sending allowed uids on the restricted network to netd.
            // TODD: Have a significant signal to know the uids has been sent to netd.
            assertBindSocketToNetworkSuccess(network);

            if (TestUtils.shouldTestTApis()) {
                // Uid is in allowed list. Try file network request again.
                requestNetwork(restrictedRequest, restrictedNetworkCb);
                // Verify that the network is restricted.
                restrictedNetworkCb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
                        NETWORK_CALLBACK_TIMEOUT_MS,
                        entry -> network.equals(entry.getNetwork())
                                && (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
                                .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)));
            }
        } finally {
            agent.unregister();

            // Restore setting.
            runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
                    mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
        }
    }

    @Test
    public void testDump() throws Exception {
        final String dumpOutput = DumpTestUtils.dumpServiceWithShellPermission(
                Context.CONNECTIVITY_SERVICE, "--short");
        assertTrue(dumpOutput, dumpOutput.contains("Active default network"));
    }

    @Test @IgnoreUpTo(SC_V2)
    public void testDumpBpfNetMaps() throws Exception {
        final String[] args = new String[] {"--short", "trafficcontroller"};
        String dumpOutput = DumpTestUtils.dumpServiceWithShellPermission(
                Context.CONNECTIVITY_SERVICE, args);
        assertTrue(dumpOutput, dumpOutput.contains("TrafficController"));
        assertFalse(dumpOutput, dumpOutput.contains("BPF map content"));

        dumpOutput = DumpTestUtils.dumpServiceWithShellPermission(
                Context.CONNECTIVITY_SERVICE, args[1]);
        assertTrue(dumpOutput, dumpOutput.contains("BPF map content"));
    }

    private void checkFirewallBlocking(final DatagramSocket srcSock, final DatagramSocket dstSock,
            final boolean expectBlock) throws Exception {
        final Random random = new Random();
        final byte[] sendData = new byte[100];
        random.nextBytes(sendData);

        final DatagramPacket pkt = new DatagramPacket(sendData, sendData.length,
                InetAddresses.parseNumericAddress("::1"), dstSock.getLocalPort());
        try {
            srcSock.send(pkt);
        } catch (IOException e) {
            if (expectBlock) {
                return;
            }
            fail("Expect not to be blocked by firewall but sending packet was blocked");
        }

        if (expectBlock) {
            fail("Expect to be blocked by firewall but sending packet was not blocked");
        }

        dstSock.receive(pkt);
        assertArrayEquals(sendData, pkt.getData());
    }

    private static final boolean EXPECT_PASS = false;
    private static final boolean EXPECT_BLOCK = true;

    private void doTestFirewallBlockingDenyRule(final int chain) {
        runWithShellPermissionIdentity(() -> {
            try (DatagramSocket srcSock = new DatagramSocket();
                 DatagramSocket dstSock = new DatagramSocket()) {
                dstSock.setSoTimeout(SOCKET_TIMEOUT_MS);

                // No global config, No uid config
                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);

                // Has global config, No uid config
                mCm.setFirewallChainEnabled(chain, true /* enable */);
                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);

                // Has global config, Has uid config
                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
                checkFirewallBlocking(srcSock, dstSock, EXPECT_BLOCK);

                // No global config, Has uid config
                mCm.setFirewallChainEnabled(chain, false /* enable */);
                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);

                // No global config, No uid config
                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
            } finally {
                mCm.setFirewallChainEnabled(chain, false /* enable */);
                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
            }
        }, NETWORK_SETTINGS);
    }

    private void doTestFirewallBlockingAllowRule(final int chain) {
        runWithShellPermissionIdentity(() -> {
            try (DatagramSocket srcSock = new DatagramSocket();
                 DatagramSocket dstSock = new DatagramSocket()) {
                dstSock.setSoTimeout(SOCKET_TIMEOUT_MS);

                // No global config, No uid config
                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);

                // Has global config, No uid config
                mCm.setFirewallChainEnabled(chain, true /* enable */);
                checkFirewallBlocking(srcSock, dstSock, EXPECT_BLOCK);

                // Has global config, Has uid config
                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);

                // No global config, Has uid config
                mCm.setFirewallChainEnabled(chain, false /* enable */);
                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);

                // No global config, No uid config
                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
                checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
            } finally {
                mCm.setFirewallChainEnabled(chain, false /* enable */);
                mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
            }
        }, NETWORK_SETTINGS);
    }

    @Test @IgnoreUpTo(SC_V2)
    @AppModeFull(reason = "Socket cannot bind in instant app mode")
    public void testFirewallBlocking() {
        // Following tests affect the actual state of networking on the device after the test.
        // This might cause unexpected behaviour of the device. So, we skip them for now.
        // We will enable following tests after adding the logic of firewall state restoring.
        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_DOZABLE);
        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_POWERSAVE);
        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_RESTRICTED);
        // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_LOW_POWER_STANDBY);

        // doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_STANDBY);
        doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_OEM_DENY_1);
        doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_OEM_DENY_2);
        doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_OEM_DENY_3);
    }

    private void assumeTestSApis() {
        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
        // shims, and @IgnoreUpTo does not check that.
        assumeTrue(TestUtils.shouldTestSApis());
    }

    private void unregisterRegisteredCallbacks() {
        for (NetworkCallback callback: mRegisteredCallbacks) {
            mCm.unregisterNetworkCallback(callback);
        }
    }

    private void registerDefaultNetworkCallback(NetworkCallback callback) {
        mCm.registerDefaultNetworkCallback(callback);
        mRegisteredCallbacks.add(callback);
    }

    private void registerDefaultNetworkCallback(NetworkCallback callback, Handler handler) {
        mCm.registerDefaultNetworkCallback(callback, handler);
        mRegisteredCallbacks.add(callback);
    }

    private void registerNetworkCallback(NetworkRequest request, NetworkCallback callback) {
        mCm.registerNetworkCallback(request, callback);
        mRegisteredCallbacks.add(callback);
    }

    private void registerSystemDefaultNetworkCallback(NetworkCallback callback, Handler handler) {
        mCmShim.registerSystemDefaultNetworkCallback(callback, handler);
        mRegisteredCallbacks.add(callback);
    }

    private void registerDefaultNetworkCallbackForUid(int uid, NetworkCallback callback,
            Handler handler) throws Exception {
        mCmShim.registerDefaultNetworkCallbackForUid(uid, callback, handler);
        mRegisteredCallbacks.add(callback);
    }

    private void requestNetwork(NetworkRequest request, NetworkCallback callback) {
        mCm.requestNetwork(request, callback);
        mRegisteredCallbacks.add(callback);
    }

    private void requestNetwork(NetworkRequest request, NetworkCallback callback, int timeoutSec) {
        mCm.requestNetwork(request, callback, timeoutSec);
        mRegisteredCallbacks.add(callback);
    }

    private void registerBestMatchingNetworkCallback(NetworkRequest request,
            NetworkCallback callback, Handler handler) {
        mCm.registerBestMatchingNetworkCallback(request, callback, handler);
        mRegisteredCallbacks.add(callback);
    }

    private void requestBackgroundNetwork(NetworkRequest request, NetworkCallback callback,
            Handler handler) throws Exception {
        mCmShim.requestBackgroundNetwork(request, callback, handler);
        mRegisteredCallbacks.add(callback);
    }
}
