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