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