| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.net.cts.util; |
| |
| import static android.Manifest.permission.NETWORK_SETTINGS; |
| import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; |
| import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; |
| import static android.net.NetworkCapabilities.TRANSPORT_TEST; |
| |
| import static com.android.compatibility.common.util.PropertyUtil.getFirstApiLevel; |
| import static com.android.testutils.TestPermissionUtil.runAsShell; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import android.annotation.NonNull; |
| import android.app.AppOpsManager; |
| import android.content.BroadcastReceiver; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageManager; |
| import android.net.ConnectivityManager; |
| import android.net.ConnectivityManager.NetworkCallback; |
| import android.net.LinkProperties; |
| import android.net.Network; |
| import android.net.NetworkCapabilities; |
| import android.net.NetworkInfo; |
| import android.net.NetworkInfo.State; |
| import android.net.NetworkRequest; |
| import android.net.TestNetworkManager; |
| import android.net.wifi.WifiInfo; |
| import android.net.wifi.WifiManager; |
| import android.os.Binder; |
| import android.os.Build; |
| import android.os.ConditionVariable; |
| import android.os.IBinder; |
| import android.system.Os; |
| import android.system.OsConstants; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import androidx.annotation.Nullable; |
| |
| import com.android.compatibility.common.util.PollingCheck; |
| import com.android.compatibility.common.util.ShellIdentityUtils; |
| import com.android.compatibility.common.util.SystemUtil; |
| import com.android.net.module.util.ConnectivitySettingsUtils; |
| import com.android.testutils.ConnectUtil; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.InetSocketAddress; |
| import java.net.Socket; |
| import java.util.ArrayList; |
| import java.util.Objects; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| |
| public final class CtsNetUtils { |
| private static final String TAG = CtsNetUtils.class.getSimpleName(); |
| |
| // Redefine this flag here so that IPsec code shipped in a mainline module can build on old |
| // platforms before FEATURE_IPSEC_TUNNEL_MIGRATION API is released. |
| // TODO: b/275378783 Remove this flag and use the platform API when it is available. |
| private static final String FEATURE_IPSEC_TUNNEL_MIGRATION = |
| "android.software.ipsec_tunnel_migration"; |
| |
| private static final int SOCKET_TIMEOUT_MS = 10_000; |
| private static final int PRIVATE_DNS_PROBE_MS = 1_000; |
| |
| private static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 10_000; |
| private static final int CONNECTIVITY_CHANGE_TIMEOUT_SECS = 30; |
| |
| private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic"; |
| private static final String PRIVATE_DNS_MODE_STRICT = "hostname"; |
| public static final int HTTP_PORT = 80; |
| public static final String TEST_HOST = "connectivitycheck.gstatic.com"; |
| public static final String HTTP_REQUEST = |
| "GET /generate_204 HTTP/1.0\r\n" + |
| "Host: " + TEST_HOST + "\r\n" + |
| "Connection: keep-alive\r\n\r\n"; |
| // Action sent to ConnectivityActionReceiver when a network callback is sent via PendingIntent. |
| public static final String NETWORK_CALLBACK_ACTION = |
| "ConnectivityManagerTest.NetworkCallbackAction"; |
| |
| private final IBinder mBinder = new Binder(); |
| private final Context mContext; |
| private final ConnectivityManager mCm; |
| private final ContentResolver mCR; |
| private final WifiManager mWifiManager; |
| private TestNetworkCallback mCellNetworkCallback; |
| private int mOldPrivateDnsMode = 0; |
| private String mOldPrivateDnsSpecifier; |
| |
| public CtsNetUtils(Context context) { |
| mContext = context; |
| mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); |
| mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); |
| mCR = context.getContentResolver(); |
| } |
| |
| /** Checks if FEATURE_IPSEC_TUNNELS is enabled on the device */ |
| public boolean hasIpsecTunnelsFeature() { |
| return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS) |
| || getFirstApiLevel() >= Build.VERSION_CODES.Q; |
| } |
| |
| /** Checks if FEATURE_IPSEC_TUNNEL_MIGRATION is enabled on the device */ |
| public boolean hasIpsecTunnelMigrateFeature() { |
| return mContext.getPackageManager().hasSystemFeature(FEATURE_IPSEC_TUNNEL_MIGRATION); |
| } |
| |
| /** |
| * Sets the given appop using shell commands |
| * |
| * <p>Expects caller to hold the shell permission identity. |
| */ |
| public void setAppopPrivileged(int appop, boolean allow) { |
| final String opName = AppOpsManager.opToName(appop); |
| for (final String pkg : new String[] {"com.android.shell", mContext.getPackageName()}) { |
| final String cmd = |
| String.format( |
| "appops set %s %s %s", |
| pkg, // Package name |
| opName, // Appop |
| (allow ? "allow" : "deny")); // Action |
| SystemUtil.runShellCommand(cmd); |
| } |
| } |
| |
| /** Sets up a test network using the provided interface name */ |
| public TestNetworkCallback setupAndGetTestNetwork(String ifname) throws Exception { |
| // Build a network request |
| final NetworkRequest nr = |
| new NetworkRequest.Builder() |
| .clearCapabilities() |
| .addTransportType(TRANSPORT_TEST) |
| .setNetworkSpecifier(ifname) |
| .build(); |
| |
| final TestNetworkCallback cb = new TestNetworkCallback(); |
| mCm.requestNetwork(nr, cb); |
| |
| // Setup the test network after network request is filed to prevent Network from being |
| // reaped due to no requests matching it. |
| mContext.getSystemService(TestNetworkManager.class).setupTestNetwork(ifname, mBinder); |
| |
| return cb; |
| } |
| |
| // Toggle WiFi twice, leaving it in the state it started in |
| public void toggleWifi() throws Exception { |
| if (mWifiManager.isWifiEnabled()) { |
| Network wifiNetwork = getWifiNetwork(); |
| // Ensure system default network is WIFI because it's expected in disconnectFromWifi() |
| expectNetworkIsSystemDefault(wifiNetwork); |
| disconnectFromWifi(wifiNetwork); |
| connectToWifi(); |
| } else { |
| connectToWifi(); |
| Network wifiNetwork = getWifiNetwork(); |
| // Ensure system default network is WIFI because it's expected in disconnectFromWifi() |
| expectNetworkIsSystemDefault(wifiNetwork); |
| disconnectFromWifi(wifiNetwork); |
| } |
| } |
| |
| public Network expectNetworkIsSystemDefault(Network network) |
| throws Exception { |
| final CompletableFuture<Network> future = new CompletableFuture(); |
| final NetworkCallback cb = new NetworkCallback() { |
| @Override |
| public void onAvailable(Network n) { |
| if (n.equals(network)) future.complete(network); |
| } |
| }; |
| |
| try { |
| mCm.registerDefaultNetworkCallback(cb); |
| return future.get(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS); |
| } catch (TimeoutException e) { |
| throw new AssertionError("Timed out waiting for system default network to switch" |
| + " to network " + network + ". Current default network is network " |
| + mCm.getActiveNetwork(), e); |
| } finally { |
| mCm.unregisterNetworkCallback(cb); |
| } |
| } |
| |
| /** |
| * Enable WiFi and wait for it to become connected to a network. |
| * |
| * This method expects to receive a legacy broadcast on connect, which may not be sent if the |
| * network does not become default or if it is not the first network. |
| */ |
| public Network connectToWifi() { |
| return connectToWifi(true /* expectLegacyBroadcast */); |
| } |
| |
| /** |
| * Enable WiFi and wait for it to become connected to a network. |
| * |
| * A network is considered connected when a {@link NetworkRequest} with TRANSPORT_WIFI |
| * receives a {@link NetworkCallback#onAvailable(Network)} callback. |
| */ |
| public Network ensureWifiConnected() { |
| return connectToWifi(false /* expectLegacyBroadcast */); |
| } |
| |
| /** |
| * Enable WiFi and wait for it to become connected to a network. |
| * |
| * @param expectLegacyBroadcast Whether to check for a legacy CONNECTIVITY_ACTION connected |
| * broadcast. The broadcast is typically not sent if the network |
| * does not become the default network, and is not the first |
| * network to appear. |
| * @return The network that was newly connected. |
| */ |
| private Network connectToWifi(boolean expectLegacyBroadcast) { |
| ConnectivityActionReceiver receiver = new ConnectivityActionReceiver( |
| mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED); |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); |
| mContext.registerReceiver(receiver, filter); |
| |
| try { |
| final Network network = new ConnectUtil(mContext).ensureWifiConnected(); |
| if (expectLegacyBroadcast) { |
| assertTrue("CONNECTIVITY_ACTION not received after connecting to " + network, |
| receiver.waitForState()); |
| } |
| return network; |
| } catch (InterruptedException ex) { |
| throw new AssertionError("connectToWifi was interrupted", ex); |
| } finally { |
| mContext.unregisterReceiver(receiver); |
| } |
| } |
| |
| /** |
| * Disable WiFi and wait for it to become disconnected from the network. |
| * |
| * This method expects to receive a legacy broadcast on disconnect, which may not be sent if the |
| * network was not default, or was not the first network. |
| * |
| * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network |
| * is expected to be able to establish a TCP connection to a remote |
| * server before disconnecting, and to have that connection closed in |
| * the process. |
| */ |
| public void disconnectFromWifi(Network wifiNetworkToCheck) { |
| disconnectFromWifi(wifiNetworkToCheck, true /* expectLegacyBroadcast */); |
| } |
| |
| /** |
| * Disable WiFi and wait for it to become disconnected from the network. |
| * |
| * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network |
| * is expected to be able to establish a TCP connection to a remote |
| * server before disconnecting, and to have that connection closed in |
| * the process. |
| */ |
| public void ensureWifiDisconnected(Network wifiNetworkToCheck) { |
| disconnectFromWifi(wifiNetworkToCheck, false /* expectLegacyBroadcast */); |
| } |
| |
| /** |
| * Disable WiFi and wait for the connection info to be cleared. |
| */ |
| public void disableWifi() throws Exception { |
| SystemUtil.runShellCommand("svc wifi disable"); |
| PollingCheck.check( |
| "Wifi not disconnected! Current network is not null " |
| + mWifiManager.getConnectionInfo().getNetworkId(), |
| TimeUnit.SECONDS.toMillis(CONNECTIVITY_CHANGE_TIMEOUT_SECS), |
| () -> ShellIdentityUtils.invokeWithShellPermissions( |
| () -> mWifiManager.getConnectionInfo().getNetworkId()) == -1); |
| } |
| |
| /** |
| * Disable WiFi and wait for it to become disconnected from the network. |
| * |
| * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network |
| * is expected to be able to establish a TCP connection to a remote |
| * server before disconnecting, and to have that connection closed in |
| * the process. |
| * @param expectLegacyBroadcast Whether to check for a legacy CONNECTIVITY_ACTION disconnected |
| * broadcast. The broadcast is typically not sent if the network |
| * was not the default network and not the first network to appear. |
| * The check will always be skipped if the device was not connected |
| * to wifi in the first place. |
| */ |
| private void disconnectFromWifi(Network wifiNetworkToCheck, boolean expectLegacyBroadcast) { |
| final TestNetworkCallback callback = new TestNetworkCallback(); |
| mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback); |
| |
| ConnectivityActionReceiver receiver = new ConnectivityActionReceiver( |
| mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.DISCONNECTED); |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); |
| mContext.registerReceiver(receiver, filter); |
| |
| final WifiInfo wifiInfo = runAsShell(NETWORK_SETTINGS, |
| () -> mWifiManager.getConnectionInfo()); |
| final boolean wasWifiConnected = wifiInfo != null && wifiInfo.getNetworkId() != -1; |
| // Assert that we can establish a TCP connection on wifi. |
| Socket wifiBoundSocket = null; |
| if (wifiNetworkToCheck != null) { |
| assertTrue("Cannot check network " + wifiNetworkToCheck + ": wifi is not connected", |
| wasWifiConnected); |
| final NetworkCapabilities nc = mCm.getNetworkCapabilities(wifiNetworkToCheck); |
| assertNotNull("Network " + wifiNetworkToCheck + " is not connected", nc); |
| try { |
| wifiBoundSocket = getBoundSocket(wifiNetworkToCheck, TEST_HOST, HTTP_PORT); |
| testHttpRequest(wifiBoundSocket); |
| } catch (IOException e) { |
| fail("HTTP request before wifi disconnected failed with: " + e); |
| } |
| } |
| |
| try { |
| if (wasWifiConnected) { |
| // Make sure the callback is registered before turning off WiFi. |
| callback.waitForAvailable(); |
| } |
| SystemUtil.runShellCommand("svc wifi disable"); |
| if (wasWifiConnected) { |
| // Ensure we get both an onLost callback and a CONNECTIVITY_ACTION. |
| assertNotNull("Did not receive onLost callback after disabling wifi", |
| callback.waitForLost()); |
| if (expectLegacyBroadcast) { |
| assertTrue("Wifi failed to reach DISCONNECTED state.", receiver.waitForState()); |
| } |
| } |
| } catch (InterruptedException ex) { |
| fail("disconnectFromWifi was interrupted"); |
| } finally { |
| mCm.unregisterNetworkCallback(callback); |
| mContext.unregisterReceiver(receiver); |
| } |
| |
| // Check that the socket is closed when wifi disconnects. |
| if (wifiBoundSocket != null) { |
| try { |
| testHttpRequest(wifiBoundSocket); |
| fail("HTTP request should not succeed after wifi disconnects"); |
| } catch (IOException expected) { |
| assertEquals(Os.strerror(OsConstants.ECONNABORTED), expected.getMessage()); |
| } |
| } |
| } |
| |
| public Network getWifiNetwork() { |
| TestNetworkCallback callback = new TestNetworkCallback(); |
| mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback); |
| Network network = null; |
| try { |
| network = callback.waitForAvailable(); |
| } catch (InterruptedException e) { |
| fail("NetworkCallback wait was interrupted."); |
| } finally { |
| mCm.unregisterNetworkCallback(callback); |
| } |
| assertNotNull("Cannot find Network for wifi. Is wifi connected?", network); |
| return network; |
| } |
| |
| public Network connectToCell() throws InterruptedException { |
| if (cellConnectAttempted()) { |
| mCm.unregisterNetworkCallback(mCellNetworkCallback); |
| } |
| NetworkRequest cellRequest = new NetworkRequest.Builder() |
| .addTransportType(TRANSPORT_CELLULAR) |
| .addCapability(NET_CAPABILITY_INTERNET) |
| .build(); |
| mCellNetworkCallback = new TestNetworkCallback(); |
| mCm.requestNetwork(cellRequest, mCellNetworkCallback); |
| final Network cellNetwork = mCellNetworkCallback.waitForAvailable(); |
| assertNotNull("Cell network not available. " + |
| "Please ensure the device has working mobile data.", cellNetwork); |
| return cellNetwork; |
| } |
| |
| public void disconnectFromCell() { |
| if (!cellConnectAttempted()) { |
| throw new IllegalStateException("Cell connection not attempted"); |
| } |
| mCm.unregisterNetworkCallback(mCellNetworkCallback); |
| mCellNetworkCallback = null; |
| } |
| |
| public boolean cellConnectAttempted() { |
| return mCellNetworkCallback != null; |
| } |
| |
| public void tearDown() { |
| if (cellConnectAttempted()) { |
| disconnectFromCell(); |
| } |
| } |
| |
| private NetworkRequest makeWifiNetworkRequest() { |
| return new NetworkRequest.Builder() |
| .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) |
| .build(); |
| } |
| |
| public void testHttpRequest(Socket s) throws IOException { |
| OutputStream out = s.getOutputStream(); |
| InputStream in = s.getInputStream(); |
| |
| final byte[] requestBytes = HTTP_REQUEST.getBytes("UTF-8"); |
| byte[] responseBytes = new byte[4096]; |
| out.write(requestBytes); |
| in.read(responseBytes); |
| final String response = new String(responseBytes, "UTF-8"); |
| assertTrue("Received unexpected response: " + response, |
| response.startsWith("HTTP/1.0 204 No Content\r\n")); |
| } |
| |
| private Socket getBoundSocket(Network network, String host, int port) throws IOException { |
| InetSocketAddress addr = new InetSocketAddress(host, port); |
| Socket s = network.getSocketFactory().createSocket(); |
| try { |
| s.setSoTimeout(SOCKET_TIMEOUT_MS); |
| s.connect(addr, SOCKET_TIMEOUT_MS); |
| } catch (IOException e) { |
| s.close(); |
| throw e; |
| } |
| return s; |
| } |
| |
| public void storePrivateDnsSetting() { |
| mOldPrivateDnsMode = ConnectivitySettingsUtils.getPrivateDnsMode(mContext); |
| mOldPrivateDnsSpecifier = ConnectivitySettingsUtils.getPrivateDnsHostname(mContext); |
| } |
| |
| public void restorePrivateDnsSetting() throws InterruptedException { |
| if (mOldPrivateDnsMode == 0) { |
| fail("restorePrivateDnsSetting without storing settings first"); |
| } |
| |
| if (mOldPrivateDnsMode != ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) { |
| // Also restore hostname even if the value is not used since private dns is not in |
| // the strict mode to prevent setting being changed after test. |
| ConnectivitySettingsUtils.setPrivateDnsHostname(mContext, mOldPrivateDnsSpecifier); |
| ConnectivitySettingsUtils.setPrivateDnsMode(mContext, mOldPrivateDnsMode); |
| return; |
| } |
| // restore private DNS setting |
| // In case of invalid setting, set to opportunistic to avoid a bad state and fail |
| if (TextUtils.isEmpty(mOldPrivateDnsSpecifier)) { |
| ConnectivitySettingsUtils.setPrivateDnsMode(mContext, |
| ConnectivitySettingsUtils.PRIVATE_DNS_MODE_OPPORTUNISTIC); |
| fail("Invalid private DNS setting: no hostname specified in strict mode"); |
| } |
| setPrivateDnsStrictMode(mOldPrivateDnsSpecifier); |
| |
| // There might be a race before private DNS setting is applied and the next test is |
| // running. So waiting private DNS to be validated can reduce the flaky rate of test. |
| awaitPrivateDnsSetting("restorePrivateDnsSetting timeout", |
| mCm.getActiveNetwork(), |
| mOldPrivateDnsSpecifier, true /* requiresValidatedServer */); |
| } |
| |
| public void setPrivateDnsStrictMode(String server) { |
| // To reduce flake rate, set PRIVATE_DNS_SPECIFIER before PRIVATE_DNS_MODE. This ensures |
| // that if the previous private DNS mode was not strict, the system only sees one |
| // EVENT_PRIVATE_DNS_SETTINGS_CHANGED event instead of two. |
| ConnectivitySettingsUtils.setPrivateDnsHostname(mContext, server); |
| final int mode = ConnectivitySettingsUtils.getPrivateDnsMode(mContext); |
| // If current private DNS mode is strict, we only need to set PRIVATE_DNS_SPECIFIER. |
| if (mode != ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) { |
| ConnectivitySettingsUtils.setPrivateDnsMode(mContext, |
| ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME); |
| } |
| } |
| |
| /** |
| * Waiting for the new private DNS setting to be validated. |
| * This method is helpful when the new private DNS setting is configured and ensure the new |
| * setting is applied and workable. It can also reduce the flaky rate when the next test is |
| * running. |
| * |
| * @param msg A message that will be printed when the validation of private DNS is timeout. |
| * @param network A network which will apply the new private DNS setting. |
| * @param server The hostname of private DNS. |
| * @param requiresValidatedServer A boolean to decide if it's needed to wait private DNS to be |
| * validated or not. |
| * @throws InterruptedException If the thread is interrupted. |
| */ |
| public void awaitPrivateDnsSetting(@NonNull String msg, @NonNull Network network, |
| @Nullable String server, boolean requiresValidatedServer) throws InterruptedException { |
| final CountDownLatch latch = new CountDownLatch(1); |
| final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build(); |
| final NetworkCallback callback = new NetworkCallback() { |
| @Override |
| public void onLinkPropertiesChanged(Network n, LinkProperties lp) { |
| Log.i(TAG, "Link properties of network " + n + " changed to " + lp); |
| if (requiresValidatedServer && lp.getValidatedPrivateDnsServers().isEmpty()) { |
| return; |
| } |
| Log.i(TAG, "Set private DNS server to " + server); |
| if (network.equals(n) && Objects.equals(server, lp.getPrivateDnsServerName())) { |
| latch.countDown(); |
| } |
| } |
| }; |
| mCm.registerNetworkCallback(request, callback); |
| assertTrue(msg, latch.await(PRIVATE_DNS_SETTING_TIMEOUT_MS, TimeUnit.MILLISECONDS)); |
| mCm.unregisterNetworkCallback(callback); |
| // Wait some time for NetworkMonitor's private DNS probe to complete. If we do not do |
| // this, then the test could complete before the NetworkMonitor private DNS probe |
| // completes. This would result in tearDown disabling private DNS, and the NetworkMonitor |
| // private DNS probe getting stuck because there are no longer any private DNS servers to |
| // query. This then results in the next test not being able to change the private DNS |
| // setting within the timeout, because the NetworkMonitor thread is blocked in the |
| // private DNS probe. There is no way to know when the probe has completed: because the |
| // network is likely already validated, there is no callback that we can listen to, so |
| // just sleep. |
| if (requiresValidatedServer) { |
| Thread.sleep(PRIVATE_DNS_PROBE_MS); |
| } |
| } |
| |
| /** |
| * Get all testable Networks with internet capability. |
| */ |
| public Network[] getTestableNetworks() { |
| final ArrayList<Network> testableNetworks = new ArrayList<Network>(); |
| for (Network network : mCm.getAllNetworks()) { |
| final NetworkCapabilities nc = mCm.getNetworkCapabilities(network); |
| if (nc != null |
| && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) |
| && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { |
| testableNetworks.add(network); |
| } |
| } |
| |
| assertTrue("This test requires that at least one public Internet-providing" |
| + " network be connected. Please ensure that the device is connected to" |
| + " a network.", |
| testableNetworks.size() >= 1); |
| return testableNetworks.toArray(new Network[0]); |
| } |
| |
| /** |
| * Receiver that captures the last connectivity change's network type and state. Recognizes |
| * both {@code CONNECTIVITY_ACTION} and {@code NETWORK_CALLBACK_ACTION} intents. |
| */ |
| public static class ConnectivityActionReceiver extends BroadcastReceiver { |
| |
| private final CountDownLatch mReceiveLatch = new CountDownLatch(1); |
| |
| private final int mNetworkType; |
| private final NetworkInfo.State mNetState; |
| private final ConnectivityManager mCm; |
| |
| public ConnectivityActionReceiver(ConnectivityManager cm, int networkType, |
| NetworkInfo.State netState) { |
| this.mCm = cm; |
| mNetworkType = networkType; |
| mNetState = netState; |
| } |
| |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| NetworkInfo networkInfo = null; |
| |
| // When receiving ConnectivityManager.CONNECTIVITY_ACTION, the NetworkInfo parcelable |
| // is stored in EXTRA_NETWORK_INFO. With a NETWORK_CALLBACK_ACTION, the Network is |
| // sent in EXTRA_NETWORK and we need to ask the ConnectivityManager for the NetworkInfo. |
| if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) { |
| networkInfo = intent.getExtras() |
| .getParcelable(ConnectivityManager.EXTRA_NETWORK_INFO); |
| assertNotNull("ConnectivityActionReceiver expected EXTRA_NETWORK_INFO", |
| networkInfo); |
| } else if (NETWORK_CALLBACK_ACTION.equals(action)) { |
| Network network = intent.getExtras() |
| .getParcelable(ConnectivityManager.EXTRA_NETWORK); |
| assertNotNull("ConnectivityActionReceiver expected EXTRA_NETWORK", network); |
| networkInfo = this.mCm.getNetworkInfo(network); |
| if (networkInfo == null) { |
| // When disconnecting, it seems like we get an intent sent with an invalid |
| // Network; that is, by the time we call ConnectivityManager.getNetworkInfo(), |
| // it is invalid. Ignore these. |
| Log.i(TAG, "ConnectivityActionReceiver NETWORK_CALLBACK_ACTION ignoring " |
| + "invalid network"); |
| return; |
| } |
| } else { |
| fail("ConnectivityActionReceiver received unxpected intent action: " + action); |
| } |
| |
| assertNotNull("ConnectivityActionReceiver didn't find NetworkInfo", networkInfo); |
| int networkType = networkInfo.getType(); |
| State networkState = networkInfo.getState(); |
| Log.i(TAG, "Network type: " + networkType + " state: " + networkState); |
| if (networkType == mNetworkType && networkInfo.getState() == mNetState) { |
| mReceiveLatch.countDown(); |
| } |
| } |
| |
| public boolean waitForState() throws InterruptedException { |
| return mReceiveLatch.await(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS); |
| } |
| } |
| |
| /** |
| * Callback used in testRegisterNetworkCallback that allows caller to block on |
| * {@code onAvailable}. |
| */ |
| public static class TestNetworkCallback extends ConnectivityManager.NetworkCallback { |
| private final ConditionVariable mAvailableCv = new ConditionVariable(false); |
| private final CountDownLatch mLostLatch = new CountDownLatch(1); |
| private final CountDownLatch mUnavailableLatch = new CountDownLatch(1); |
| |
| public Network currentNetwork; |
| public Network lastLostNetwork; |
| |
| /** |
| * Wait for a network to be available. |
| * |
| * If onAvailable was previously called but was followed by onLost, this will wait for the |
| * next available network. |
| */ |
| public Network waitForAvailable() throws InterruptedException { |
| final long timeoutMs = TimeUnit.SECONDS.toMillis(CONNECTIVITY_CHANGE_TIMEOUT_SECS); |
| while (mAvailableCv.block(timeoutMs)) { |
| final Network n = currentNetwork; |
| if (n != null) return n; |
| Log.w(TAG, "onAvailable called but network was lost before it could be returned." |
| + " Waiting for the next call to onAvailable."); |
| } |
| return null; |
| } |
| |
| public Network waitForLost() throws InterruptedException { |
| return mLostLatch.await(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS) |
| ? lastLostNetwork : null; |
| } |
| |
| public boolean waitForUnavailable() throws InterruptedException { |
| return mUnavailableLatch.await(2, TimeUnit.SECONDS); |
| } |
| |
| @Override |
| public void onAvailable(Network network) { |
| Log.i(TAG, "CtsNetUtils TestNetworkCallback onAvailable " + network); |
| currentNetwork = network; |
| mAvailableCv.open(); |
| } |
| |
| @Override |
| public void onLost(Network network) { |
| Log.i(TAG, "CtsNetUtils TestNetworkCallback onLost " + network); |
| lastLostNetwork = network; |
| if (network.equals(currentNetwork)) { |
| mAvailableCv.close(); |
| currentNetwork = null; |
| } |
| mLostLatch.countDown(); |
| } |
| |
| @Override |
| public void onUnavailable() { |
| mUnavailableLatch.countDown(); |
| } |
| } |
| } |