• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.net.cts.util;
18 
19 import static android.Manifest.permission.NETWORK_SETTINGS;
20 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
21 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
22 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
23 
24 import static com.android.compatibility.common.util.PropertyUtil.getFirstApiLevel;
25 import static com.android.testutils.TestPermissionUtil.runAsShell;
26 
27 import static org.junit.Assert.assertEquals;
28 import static org.junit.Assert.assertNotNull;
29 import static org.junit.Assert.assertTrue;
30 import static org.junit.Assert.fail;
31 
32 import android.annotation.NonNull;
33 import android.app.AppOpsManager;
34 import android.content.BroadcastReceiver;
35 import android.content.ContentResolver;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.IntentFilter;
39 import android.content.pm.PackageManager;
40 import android.net.ConnectivityManager;
41 import android.net.ConnectivityManager.NetworkCallback;
42 import android.net.LinkProperties;
43 import android.net.Network;
44 import android.net.NetworkCapabilities;
45 import android.net.NetworkInfo;
46 import android.net.NetworkInfo.State;
47 import android.net.NetworkRequest;
48 import android.net.TestNetworkManager;
49 import android.net.wifi.WifiInfo;
50 import android.net.wifi.WifiManager;
51 import android.os.Binder;
52 import android.os.Build;
53 import android.os.ConditionVariable;
54 import android.os.IBinder;
55 import android.system.Os;
56 import android.system.OsConstants;
57 import android.text.TextUtils;
58 import android.util.Log;
59 
60 import com.android.compatibility.common.util.PollingCheck;
61 import com.android.compatibility.common.util.ShellIdentityUtils;
62 import com.android.compatibility.common.util.SystemUtil;
63 import com.android.net.module.util.ConnectivitySettingsUtils;
64 import com.android.testutils.ConnectUtil;
65 
66 import java.io.IOException;
67 import java.io.InputStream;
68 import java.io.OutputStream;
69 import java.net.InetSocketAddress;
70 import java.net.Socket;
71 import java.util.concurrent.CompletableFuture;
72 import java.util.concurrent.CountDownLatch;
73 import java.util.concurrent.TimeUnit;
74 import java.util.concurrent.TimeoutException;
75 
76 public final class CtsNetUtils {
77     private static final String TAG = CtsNetUtils.class.getSimpleName();
78 
79     // Redefine this flag here so that IPsec code shipped in a mainline module can build on old
80     // platforms before FEATURE_IPSEC_TUNNEL_MIGRATION API is released.
81     // TODO: b/275378783 Remove this flag and use the platform API when it is available.
82     private static final String FEATURE_IPSEC_TUNNEL_MIGRATION =
83             "android.software.ipsec_tunnel_migration";
84 
85     private static final int SOCKET_TIMEOUT_MS = 2000;
86     private static final int PRIVATE_DNS_PROBE_MS = 1_000;
87 
88     private static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 10_000;
89     private static final int CONNECTIVITY_CHANGE_TIMEOUT_SECS = 30;
90 
91     private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic";
92     private static final String PRIVATE_DNS_MODE_STRICT = "hostname";
93     public static final int HTTP_PORT = 80;
94     public static final String TEST_HOST = "connectivitycheck.gstatic.com";
95     public static final String HTTP_REQUEST =
96             "GET /generate_204 HTTP/1.0\r\n" +
97                     "Host: " + TEST_HOST + "\r\n" +
98                     "Connection: keep-alive\r\n\r\n";
99     // Action sent to ConnectivityActionReceiver when a network callback is sent via PendingIntent.
100     public static final String NETWORK_CALLBACK_ACTION =
101             "ConnectivityManagerTest.NetworkCallbackAction";
102 
103     private final IBinder mBinder = new Binder();
104     private final Context mContext;
105     private final ConnectivityManager mCm;
106     private final ContentResolver mCR;
107     private final WifiManager mWifiManager;
108     private TestNetworkCallback mCellNetworkCallback;
109     private int mOldPrivateDnsMode = 0;
110     private String mOldPrivateDnsSpecifier;
111 
CtsNetUtils(Context context)112     public CtsNetUtils(Context context) {
113         mContext = context;
114         mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
115         mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
116         mCR = context.getContentResolver();
117     }
118 
119     /** Checks if FEATURE_IPSEC_TUNNELS is enabled on the device */
hasIpsecTunnelsFeature()120     public boolean hasIpsecTunnelsFeature() {
121         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
122                 || getFirstApiLevel() >= Build.VERSION_CODES.Q;
123     }
124 
125     /** Checks if FEATURE_IPSEC_TUNNEL_MIGRATION is enabled on the device */
hasIpsecTunnelMigrateFeature()126     public boolean hasIpsecTunnelMigrateFeature() {
127         return mContext.getPackageManager().hasSystemFeature(FEATURE_IPSEC_TUNNEL_MIGRATION);
128     }
129 
130     /**
131      * Sets the given appop using shell commands
132      *
133      * <p>Expects caller to hold the shell permission identity.
134      */
setAppopPrivileged(int appop, boolean allow)135     public void setAppopPrivileged(int appop, boolean allow) {
136         final String opName = AppOpsManager.opToName(appop);
137         for (final String pkg : new String[] {"com.android.shell", mContext.getPackageName()}) {
138             final String cmd =
139                     String.format(
140                             "appops set %s %s %s",
141                             pkg, // Package name
142                             opName, // Appop
143                             (allow ? "allow" : "deny")); // Action
144             SystemUtil.runShellCommand(cmd);
145         }
146     }
147 
148     /** Sets up a test network using the provided interface name */
setupAndGetTestNetwork(String ifname)149     public TestNetworkCallback setupAndGetTestNetwork(String ifname) throws Exception {
150         // Build a network request
151         final NetworkRequest nr =
152                 new NetworkRequest.Builder()
153                         .clearCapabilities()
154                         .addTransportType(TRANSPORT_TEST)
155                         .setNetworkSpecifier(ifname)
156                         .build();
157 
158         final TestNetworkCallback cb = new TestNetworkCallback();
159         mCm.requestNetwork(nr, cb);
160 
161         // Setup the test network after network request is filed to prevent Network from being
162         // reaped due to no requests matching it.
163         mContext.getSystemService(TestNetworkManager.class).setupTestNetwork(ifname, mBinder);
164 
165         return cb;
166     }
167 
168     // Toggle WiFi twice, leaving it in the state it started in
toggleWifi()169     public void toggleWifi() throws Exception {
170         if (mWifiManager.isWifiEnabled()) {
171             Network wifiNetwork = getWifiNetwork();
172             // Ensure system default network is WIFI because it's expected in disconnectFromWifi()
173             expectNetworkIsSystemDefault(wifiNetwork);
174             disconnectFromWifi(wifiNetwork);
175             connectToWifi();
176         } else {
177             connectToWifi();
178             Network wifiNetwork = getWifiNetwork();
179             // Ensure system default network is WIFI because it's expected in disconnectFromWifi()
180             expectNetworkIsSystemDefault(wifiNetwork);
181             disconnectFromWifi(wifiNetwork);
182         }
183     }
184 
expectNetworkIsSystemDefault(Network network)185     public Network expectNetworkIsSystemDefault(Network network)
186             throws Exception {
187         final CompletableFuture<Network> future = new CompletableFuture();
188         final NetworkCallback cb = new NetworkCallback() {
189             @Override
190             public void onAvailable(Network n) {
191                 if (n.equals(network)) future.complete(network);
192             }
193         };
194 
195         try {
196             mCm.registerDefaultNetworkCallback(cb);
197             return future.get(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS);
198         } catch (TimeoutException e) {
199             throw new AssertionError("Timed out waiting for system default network to switch"
200                     + " to network " + network + ". Current default network is network "
201                     + mCm.getActiveNetwork(), e);
202         } finally {
203             mCm.unregisterNetworkCallback(cb);
204         }
205     }
206 
207     /**
208      * Enable WiFi and wait for it to become connected to a network.
209      *
210      * This method expects to receive a legacy broadcast on connect, which may not be sent if the
211      * network does not become default or if it is not the first network.
212      */
connectToWifi()213     public Network connectToWifi() {
214         return connectToWifi(true /* expectLegacyBroadcast */);
215     }
216 
217     /**
218      * Enable WiFi and wait for it to become connected to a network.
219      *
220      * A network is considered connected when a {@link NetworkRequest} with TRANSPORT_WIFI
221      * receives a {@link NetworkCallback#onAvailable(Network)} callback.
222      */
ensureWifiConnected()223     public Network ensureWifiConnected() {
224         return connectToWifi(false /* expectLegacyBroadcast */);
225     }
226 
227     /**
228      * Enable WiFi and wait for it to become connected to a network.
229      *
230      * @param expectLegacyBroadcast Whether to check for a legacy CONNECTIVITY_ACTION connected
231      *                              broadcast. The broadcast is typically not sent if the network
232      *                              does not become the default network, and is not the first
233      *                              network to appear.
234      * @return The network that was newly connected.
235      */
connectToWifi(boolean expectLegacyBroadcast)236     private Network connectToWifi(boolean expectLegacyBroadcast) {
237         ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
238                 mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED);
239         IntentFilter filter = new IntentFilter();
240         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
241         mContext.registerReceiver(receiver, filter);
242 
243         try {
244             final Network network = new ConnectUtil(mContext).ensureWifiConnected();
245             if (expectLegacyBroadcast) {
246                 assertTrue("CONNECTIVITY_ACTION not received after connecting to " + network,
247                         receiver.waitForState());
248             }
249             return network;
250         } catch (InterruptedException ex) {
251             throw new AssertionError("connectToWifi was interrupted", ex);
252         } finally {
253             mContext.unregisterReceiver(receiver);
254         }
255     }
256 
257     /**
258      * Disable WiFi and wait for it to become disconnected from the network.
259      *
260      * This method expects to receive a legacy broadcast on disconnect, which may not be sent if the
261      * network was not default, or was not the first network.
262      *
263      * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network
264      *                           is expected to be able to establish a TCP connection to a remote
265      *                           server before disconnecting, and to have that connection closed in
266      *                           the process.
267      */
disconnectFromWifi(Network wifiNetworkToCheck)268     public void disconnectFromWifi(Network wifiNetworkToCheck) {
269         disconnectFromWifi(wifiNetworkToCheck, true /* expectLegacyBroadcast */);
270     }
271 
272     /**
273      * Disable WiFi and wait for it to become disconnected from the network.
274      *
275      * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network
276      *                           is expected to be able to establish a TCP connection to a remote
277      *                           server before disconnecting, and to have that connection closed in
278      *                           the process.
279      */
ensureWifiDisconnected(Network wifiNetworkToCheck)280     public void ensureWifiDisconnected(Network wifiNetworkToCheck) {
281         disconnectFromWifi(wifiNetworkToCheck, false /* expectLegacyBroadcast */);
282     }
283 
284     /**
285      * Disable WiFi and wait for the connection info to be cleared.
286      */
disableWifi()287     public void disableWifi() throws Exception {
288         SystemUtil.runShellCommand("svc wifi disable");
289         PollingCheck.check(
290                 "Wifi not disconnected! Current network is not null "
291                         + mWifiManager.getConnectionInfo().getNetworkId(),
292                 TimeUnit.SECONDS.toMillis(CONNECTIVITY_CHANGE_TIMEOUT_SECS),
293                 () -> ShellIdentityUtils.invokeWithShellPermissions(
294                         () -> mWifiManager.getConnectionInfo().getNetworkId()) == -1);
295     }
296 
297     /**
298      * Disable WiFi and wait for it to become disconnected from the network.
299      *
300      * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network
301      *                           is expected to be able to establish a TCP connection to a remote
302      *                           server before disconnecting, and to have that connection closed in
303      *                           the process.
304      * @param expectLegacyBroadcast Whether to check for a legacy CONNECTIVITY_ACTION disconnected
305      *                              broadcast. The broadcast is typically not sent if the network
306      *                              was not the default network and not the first network to appear.
307      *                              The check will always be skipped if the device was not connected
308      *                              to wifi in the first place.
309      */
disconnectFromWifi(Network wifiNetworkToCheck, boolean expectLegacyBroadcast)310     private void disconnectFromWifi(Network wifiNetworkToCheck, boolean expectLegacyBroadcast) {
311         final TestNetworkCallback callback = new TestNetworkCallback();
312         mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback);
313 
314         ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
315                 mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.DISCONNECTED);
316         IntentFilter filter = new IntentFilter();
317         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
318         mContext.registerReceiver(receiver, filter);
319 
320         final WifiInfo wifiInfo = runAsShell(NETWORK_SETTINGS,
321                 () -> mWifiManager.getConnectionInfo());
322         final boolean wasWifiConnected = wifiInfo != null && wifiInfo.getNetworkId() != -1;
323         // Assert that we can establish a TCP connection on wifi.
324         Socket wifiBoundSocket = null;
325         if (wifiNetworkToCheck != null) {
326             assertTrue("Cannot check network " + wifiNetworkToCheck + ": wifi is not connected",
327                     wasWifiConnected);
328             final NetworkCapabilities nc = mCm.getNetworkCapabilities(wifiNetworkToCheck);
329             assertNotNull("Network " + wifiNetworkToCheck + " is not connected", nc);
330             try {
331                 wifiBoundSocket = getBoundSocket(wifiNetworkToCheck, TEST_HOST, HTTP_PORT);
332                 testHttpRequest(wifiBoundSocket);
333             } catch (IOException e) {
334                 fail("HTTP request before wifi disconnected failed with: " + e);
335             }
336         }
337 
338         try {
339             if (wasWifiConnected) {
340                 // Make sure the callback is registered before turning off WiFi.
341                 callback.waitForAvailable();
342             }
343             SystemUtil.runShellCommand("svc wifi disable");
344             if (wasWifiConnected) {
345                 // Ensure we get both an onLost callback and a CONNECTIVITY_ACTION.
346                 assertNotNull("Did not receive onLost callback after disabling wifi",
347                         callback.waitForLost());
348                 if (expectLegacyBroadcast) {
349                     assertTrue("Wifi failed to reach DISCONNECTED state.", receiver.waitForState());
350                 }
351             }
352         } catch (InterruptedException ex) {
353             fail("disconnectFromWifi was interrupted");
354         } finally {
355             mCm.unregisterNetworkCallback(callback);
356             mContext.unregisterReceiver(receiver);
357         }
358 
359         // Check that the socket is closed when wifi disconnects.
360         if (wifiBoundSocket != null) {
361             try {
362                 testHttpRequest(wifiBoundSocket);
363                 fail("HTTP request should not succeed after wifi disconnects");
364             } catch (IOException expected) {
365                 assertEquals(Os.strerror(OsConstants.ECONNABORTED), expected.getMessage());
366             }
367         }
368     }
369 
getWifiNetwork()370     public Network getWifiNetwork() {
371         TestNetworkCallback callback = new TestNetworkCallback();
372         mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback);
373         Network network = null;
374         try {
375             network = callback.waitForAvailable();
376         } catch (InterruptedException e) {
377             fail("NetworkCallback wait was interrupted.");
378         } finally {
379             mCm.unregisterNetworkCallback(callback);
380         }
381         assertNotNull("Cannot find Network for wifi. Is wifi connected?", network);
382         return network;
383     }
384 
connectToCell()385     public Network connectToCell() throws InterruptedException {
386         if (cellConnectAttempted()) {
387             mCm.unregisterNetworkCallback(mCellNetworkCallback);
388         }
389         NetworkRequest cellRequest = new NetworkRequest.Builder()
390                 .addTransportType(TRANSPORT_CELLULAR)
391                 .addCapability(NET_CAPABILITY_INTERNET)
392                 .build();
393         mCellNetworkCallback = new TestNetworkCallback();
394         mCm.requestNetwork(cellRequest, mCellNetworkCallback);
395         final Network cellNetwork = mCellNetworkCallback.waitForAvailable();
396         assertNotNull("Cell network not available. " +
397                 "Please ensure the device has working mobile data.", cellNetwork);
398         return cellNetwork;
399     }
400 
disconnectFromCell()401     public void disconnectFromCell() {
402         if (!cellConnectAttempted()) {
403             throw new IllegalStateException("Cell connection not attempted");
404         }
405         mCm.unregisterNetworkCallback(mCellNetworkCallback);
406         mCellNetworkCallback = null;
407     }
408 
cellConnectAttempted()409     public boolean cellConnectAttempted() {
410         return mCellNetworkCallback != null;
411     }
412 
tearDown()413     public void tearDown() {
414         if (cellConnectAttempted()) {
415             disconnectFromCell();
416         }
417     }
418 
makeWifiNetworkRequest()419     private NetworkRequest makeWifiNetworkRequest() {
420         return new NetworkRequest.Builder()
421                 .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
422                 .build();
423     }
424 
testHttpRequest(Socket s)425     public void testHttpRequest(Socket s) throws IOException {
426         OutputStream out = s.getOutputStream();
427         InputStream in = s.getInputStream();
428 
429         final byte[] requestBytes = HTTP_REQUEST.getBytes("UTF-8");
430         byte[] responseBytes = new byte[4096];
431         out.write(requestBytes);
432         in.read(responseBytes);
433         final String response = new String(responseBytes, "UTF-8");
434         assertTrue("Received unexpected response: " + response,
435                 response.startsWith("HTTP/1.0 204 No Content\r\n"));
436     }
437 
getBoundSocket(Network network, String host, int port)438     private Socket getBoundSocket(Network network, String host, int port) throws IOException {
439         InetSocketAddress addr = new InetSocketAddress(host, port);
440         Socket s = network.getSocketFactory().createSocket();
441         try {
442             s.setSoTimeout(SOCKET_TIMEOUT_MS);
443             s.connect(addr, SOCKET_TIMEOUT_MS);
444         } catch (IOException e) {
445             s.close();
446             throw e;
447         }
448         return s;
449     }
450 
storePrivateDnsSetting()451     public void storePrivateDnsSetting() {
452         mOldPrivateDnsMode = ConnectivitySettingsUtils.getPrivateDnsMode(mContext);
453         mOldPrivateDnsSpecifier = ConnectivitySettingsUtils.getPrivateDnsHostname(mContext);
454     }
455 
restorePrivateDnsSetting()456     public void restorePrivateDnsSetting() throws InterruptedException {
457         if (mOldPrivateDnsMode == 0) {
458             fail("restorePrivateDnsSetting without storing settings first");
459         }
460 
461         if (mOldPrivateDnsMode != ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) {
462             // Also restore hostname even if the value is not used since private dns is not in
463             // the strict mode to prevent setting being changed after test.
464             ConnectivitySettingsUtils.setPrivateDnsHostname(mContext, mOldPrivateDnsSpecifier);
465             ConnectivitySettingsUtils.setPrivateDnsMode(mContext, mOldPrivateDnsMode);
466             return;
467         }
468         // restore private DNS setting
469         // In case of invalid setting, set to opportunistic to avoid a bad state and fail
470         if (TextUtils.isEmpty(mOldPrivateDnsSpecifier)) {
471             ConnectivitySettingsUtils.setPrivateDnsMode(mContext,
472                     ConnectivitySettingsUtils.PRIVATE_DNS_MODE_OPPORTUNISTIC);
473             fail("Invalid private DNS setting: no hostname specified in strict mode");
474         }
475         setPrivateDnsStrictMode(mOldPrivateDnsSpecifier);
476 
477         // There might be a race before private DNS setting is applied and the next test is
478         // running. So waiting private DNS to be validated can reduce the flaky rate of test.
479         awaitPrivateDnsSetting("restorePrivateDnsSetting timeout",
480                 mCm.getActiveNetwork(),
481                 mOldPrivateDnsSpecifier, true /* requiresValidatedServer */);
482     }
483 
setPrivateDnsStrictMode(String server)484     public void setPrivateDnsStrictMode(String server) {
485         // To reduce flake rate, set PRIVATE_DNS_SPECIFIER before PRIVATE_DNS_MODE. This ensures
486         // that if the previous private DNS mode was not strict, the system only sees one
487         // EVENT_PRIVATE_DNS_SETTINGS_CHANGED event instead of two.
488         ConnectivitySettingsUtils.setPrivateDnsHostname(mContext, server);
489         final int mode = ConnectivitySettingsUtils.getPrivateDnsMode(mContext);
490         // If current private DNS mode is strict, we only need to set PRIVATE_DNS_SPECIFIER.
491         if (mode != ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) {
492             ConnectivitySettingsUtils.setPrivateDnsMode(mContext,
493                     ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
494         }
495     }
496 
497     /**
498      * Waiting for the new private DNS setting to be validated.
499      * This method is helpful when the new private DNS setting is configured and ensure the new
500      * setting is applied and workable. It can also reduce the flaky rate when the next test is
501      * running.
502      *
503      * @param msg A message that will be printed when the validation of private DNS is timeout.
504      * @param network A network which will apply the new private DNS setting.
505      * @param server The hostname of private DNS.
506      * @param requiresValidatedServer A boolean to decide if it's needed to wait private DNS to be
507      *                                 validated or not.
508      * @throws InterruptedException If the thread is interrupted.
509      */
awaitPrivateDnsSetting(@onNull String msg, @NonNull Network network, @NonNull String server, boolean requiresValidatedServer)510     public void awaitPrivateDnsSetting(@NonNull String msg, @NonNull Network network,
511             @NonNull String server, boolean requiresValidatedServer) throws InterruptedException {
512         final CountDownLatch latch = new CountDownLatch(1);
513         final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
514         NetworkCallback callback = new NetworkCallback() {
515             @Override
516             public void onLinkPropertiesChanged(Network n, LinkProperties lp) {
517                 Log.i(TAG, "Link properties of network " + n + " changed to " + lp);
518                 if (requiresValidatedServer && lp.getValidatedPrivateDnsServers().isEmpty()) {
519                     return;
520                 }
521                 if (network.equals(n) && server.equals(lp.getPrivateDnsServerName())) {
522                     latch.countDown();
523                 }
524             }
525         };
526         mCm.registerNetworkCallback(request, callback);
527         assertTrue(msg, latch.await(PRIVATE_DNS_SETTING_TIMEOUT_MS, TimeUnit.MILLISECONDS));
528         mCm.unregisterNetworkCallback(callback);
529         // Wait some time for NetworkMonitor's private DNS probe to complete. If we do not do
530         // this, then the test could complete before the NetworkMonitor private DNS probe
531         // completes. This would result in tearDown disabling private DNS, and the NetworkMonitor
532         // private DNS probe getting stuck because there are no longer any private DNS servers to
533         // query. This then results in the next test not being able to change the private DNS
534         // setting within the timeout, because the NetworkMonitor thread is blocked in the
535         // private DNS probe. There is no way to know when the probe has completed: because the
536         // network is likely already validated, there is no callback that we can listen to, so
537         // just sleep.
538         if (requiresValidatedServer) {
539             Thread.sleep(PRIVATE_DNS_PROBE_MS);
540         }
541     }
542 
543     /**
544      * Receiver that captures the last connectivity change's network type and state. Recognizes
545      * both {@code CONNECTIVITY_ACTION} and {@code NETWORK_CALLBACK_ACTION} intents.
546      */
547     public static class ConnectivityActionReceiver extends BroadcastReceiver {
548 
549         private final CountDownLatch mReceiveLatch = new CountDownLatch(1);
550 
551         private final int mNetworkType;
552         private final NetworkInfo.State mNetState;
553         private final ConnectivityManager mCm;
554 
ConnectivityActionReceiver(ConnectivityManager cm, int networkType, NetworkInfo.State netState)555         public ConnectivityActionReceiver(ConnectivityManager cm, int networkType,
556                 NetworkInfo.State netState) {
557             this.mCm = cm;
558             mNetworkType = networkType;
559             mNetState = netState;
560         }
561 
onReceive(Context context, Intent intent)562         public void onReceive(Context context, Intent intent) {
563             String action = intent.getAction();
564             NetworkInfo networkInfo = null;
565 
566             // When receiving ConnectivityManager.CONNECTIVITY_ACTION, the NetworkInfo parcelable
567             // is stored in EXTRA_NETWORK_INFO. With a NETWORK_CALLBACK_ACTION, the Network is
568             // sent in EXTRA_NETWORK and we need to ask the ConnectivityManager for the NetworkInfo.
569             if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
570                 networkInfo = intent.getExtras()
571                         .getParcelable(ConnectivityManager.EXTRA_NETWORK_INFO);
572                 assertNotNull("ConnectivityActionReceiver expected EXTRA_NETWORK_INFO",
573                         networkInfo);
574             } else if (NETWORK_CALLBACK_ACTION.equals(action)) {
575                 Network network = intent.getExtras()
576                         .getParcelable(ConnectivityManager.EXTRA_NETWORK);
577                 assertNotNull("ConnectivityActionReceiver expected EXTRA_NETWORK", network);
578                 networkInfo = this.mCm.getNetworkInfo(network);
579                 if (networkInfo == null) {
580                     // When disconnecting, it seems like we get an intent sent with an invalid
581                     // Network; that is, by the time we call ConnectivityManager.getNetworkInfo(),
582                     // it is invalid. Ignore these.
583                     Log.i(TAG, "ConnectivityActionReceiver NETWORK_CALLBACK_ACTION ignoring "
584                             + "invalid network");
585                     return;
586                 }
587             } else {
588                 fail("ConnectivityActionReceiver received unxpected intent action: " + action);
589             }
590 
591             assertNotNull("ConnectivityActionReceiver didn't find NetworkInfo", networkInfo);
592             int networkType = networkInfo.getType();
593             State networkState = networkInfo.getState();
594             Log.i(TAG, "Network type: " + networkType + " state: " + networkState);
595             if (networkType == mNetworkType && networkInfo.getState() == mNetState) {
596                 mReceiveLatch.countDown();
597             }
598         }
599 
waitForState()600         public boolean waitForState() throws InterruptedException {
601             return mReceiveLatch.await(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS);
602         }
603     }
604 
605     /**
606      * Callback used in testRegisterNetworkCallback that allows caller to block on
607      * {@code onAvailable}.
608      */
609     public static class TestNetworkCallback extends ConnectivityManager.NetworkCallback {
610         private final ConditionVariable mAvailableCv = new ConditionVariable(false);
611         private final CountDownLatch mLostLatch = new CountDownLatch(1);
612         private final CountDownLatch mUnavailableLatch = new CountDownLatch(1);
613 
614         public Network currentNetwork;
615         public Network lastLostNetwork;
616 
617         /**
618          * Wait for a network to be available.
619          *
620          * If onAvailable was previously called but was followed by onLost, this will wait for the
621          * next available network.
622          */
waitForAvailable()623         public Network waitForAvailable() throws InterruptedException {
624             final long timeoutMs = TimeUnit.SECONDS.toMillis(CONNECTIVITY_CHANGE_TIMEOUT_SECS);
625             while (mAvailableCv.block(timeoutMs)) {
626                 final Network n = currentNetwork;
627                 if (n != null) return n;
628                 Log.w(TAG, "onAvailable called but network was lost before it could be returned."
629                         + " Waiting for the next call to onAvailable.");
630             }
631             return null;
632         }
633 
waitForLost()634         public Network waitForLost() throws InterruptedException {
635             return mLostLatch.await(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS)
636                     ? lastLostNetwork : null;
637         }
638 
waitForUnavailable()639         public boolean waitForUnavailable() throws InterruptedException {
640             return mUnavailableLatch.await(2, TimeUnit.SECONDS);
641         }
642 
643         @Override
onAvailable(Network network)644         public void onAvailable(Network network) {
645             Log.i(TAG, "CtsNetUtils TestNetworkCallback onAvailable " + network);
646             currentNetwork = network;
647             mAvailableCv.open();
648         }
649 
650         @Override
onLost(Network network)651         public void onLost(Network network) {
652             Log.i(TAG, "CtsNetUtils TestNetworkCallback onLost " + network);
653             lastLostNetwork = network;
654             if (network.equals(currentNetwork)) {
655                 mAvailableCv.close();
656                 currentNetwork = null;
657             }
658             mLostLatch.countDown();
659         }
660 
661         @Override
onUnavailable()662         public void onUnavailable() {
663             mUnavailableLatch.countDown();
664         }
665     }
666 }
667