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