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