xref: /aosp_15_r20/cts/tests/JobScheduler/src/android/jobscheduler/cts/NetworkingHelper.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
2  * Copyright (C) 2022 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.jobscheduler.cts;
18 
19 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
20 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
21 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
22 
23 import static com.android.compatibility.common.util.TestUtils.waitUntil;
24 
25 import static junit.framework.Assert.fail;
26 
27 import static org.junit.Assert.assertEquals;
28 import static org.junit.Assert.assertFalse;
29 import static org.junit.Assert.assertNotEquals;
30 
31 import android.Manifest;
32 import android.annotation.NonNull;
33 import android.app.Instrumentation;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.IntentFilter;
37 import android.content.pm.PackageManager;
38 import android.location.LocationManager;
39 import android.net.ConnectivityManager;
40 import android.net.Network;
41 import android.net.NetworkCapabilities;
42 import android.net.NetworkPolicyManager;
43 import android.net.NetworkRequest;
44 import android.net.wifi.WifiConfiguration;
45 import android.net.wifi.WifiManager;
46 import android.os.Handler;
47 import android.os.Looper;
48 import android.os.Message;
49 import android.provider.Settings;
50 import android.util.Log;
51 
52 import com.android.compatibility.common.util.CallbackAsserter;
53 import com.android.compatibility.common.util.ShellIdentityUtils;
54 import com.android.compatibility.common.util.SystemUtil;
55 
56 import java.util.List;
57 import java.util.concurrent.CountDownLatch;
58 import java.util.concurrent.TimeUnit;
59 import java.util.regex.Matcher;
60 import java.util.regex.Pattern;
61 
62 public class NetworkingHelper implements AutoCloseable {
63     private static final String TAG = "JsNetworkingUtils";
64 
65     private static final String RESTRICT_BACKGROUND_GET_CMD =
66             "cmd netpolicy get restrict-background";
67     private static final String RESTRICT_BACKGROUND_ON_CMD =
68             "cmd netpolicy set restrict-background true";
69     private static final String RESTRICT_BACKGROUND_OFF_CMD =
70             "cmd netpolicy set restrict-background false";
71 
72     private final Context mContext;
73     private final Instrumentation mInstrumentation;
74 
75     private final ConnectivityManager mConnectivityManager;
76     private final WifiManager mWifiManager;
77 
78     /** Whether the device running these tests supports WiFi. */
79     private final boolean mHasWifi;
80     /** Whether the device running these tests supports ethernet. */
81     private final boolean mHasEthernet;
82     /** Whether the device running these tests supports telephony. */
83     private final boolean mHasTelephony;
84 
85     private final boolean mInitialAirplaneModeState;
86     private final boolean mInitialDataSaverState;
87     private final String mInitialLocationMode;
88     private final boolean mInitialWiFiState;
89     private String mInitialWiFiMeteredState;
90     private String mInitialWiFiSSID;
91 
NetworkingHelper(@onNull Instrumentation instrumentation, @NonNull Context context)92     NetworkingHelper(@NonNull Instrumentation instrumentation, @NonNull Context context)
93             throws Exception {
94         mContext = context;
95         mInstrumentation = instrumentation;
96 
97         mConnectivityManager = context.getSystemService(ConnectivityManager.class);
98         mWifiManager = context.getSystemService(WifiManager.class);
99 
100         PackageManager packageManager = mContext.getPackageManager();
101         mHasWifi = packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI);
102         mHasEthernet = packageManager.hasSystemFeature(PackageManager.FEATURE_ETHERNET);
103         mHasTelephony = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
104 
105         mInitialAirplaneModeState = isAirplaneModeOn();
106         mInitialDataSaverState = isDataSaverEnabled();
107         mInitialLocationMode = Settings.Secure.getString(
108                 mContext.getContentResolver(), Settings.Secure.LOCATION_MODE);
109         mInitialWiFiState = mHasWifi && isWifiEnabled();
110 
111         ensureSavedWifiNetwork();
112     }
113 
114     /** Ensures that the device has a wifi network saved if it has the wifi feature. */
ensureSavedWifiNetwork()115     private void ensureSavedWifiNetwork() throws Exception {
116         if (!mHasWifi) {
117             return;
118         }
119         final List<WifiConfiguration> savedNetworks =
120                 ShellIdentityUtils.invokeMethodWithShellPermissions(
121                         mWifiManager, WifiManager::getConfiguredNetworks);
122         assertFalse("Need at least one saved wifi network", savedNetworks.isEmpty());
123 
124         setWifiState(true);
125         if (mInitialWiFiSSID == null) {
126             mInitialWiFiSSID = getWifiSSID();
127             mInitialWiFiMeteredState = getWifiMeteredStatus(mInitialWiFiSSID);
128         }
129     }
130 
131     // Returns "true", "false", or "none".
getWifiMeteredStatus(String ssid)132     private String getWifiMeteredStatus(String ssid) {
133         // Interestingly giving the SSID as an argument to list wifi-networks
134         // only works iff the network in question has the "false" policy.
135         // Also unfortunately runShellCommand does not pass the command to the interpreter
136         // so it's not possible to | grep the ssid.
137         final String command = "cmd netpolicy list wifi-networks";
138         final String policyString = SystemUtil.runShellCommand(command);
139 
140         final Matcher m = Pattern.compile(ssid + ";(true|false|none)",
141                 Pattern.MULTILINE | Pattern.UNIX_LINES).matcher(policyString);
142         if (!m.find()) {
143             fail("Unexpected format from cmd netpolicy (when looking for " + ssid + "): "
144                     + policyString);
145         }
146         return m.group(1);
147     }
148 
149     @NonNull
getWifiSSID()150     private String getWifiSSID() throws Exception {
151         // Location needs to be enabled to get the WiFi information.
152         setLocationMode(String.valueOf(Settings.Secure.LOCATION_MODE_ON));
153         final String ssid = SystemUtil.callWithShellPermissionIdentity(
154                 () -> mWifiManager.getConnectionInfo().getSSID(),
155                 Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_WIFI_STATE,
156                 Manifest.permission.INTERACT_ACROSS_USERS_FULL);
157         assertNotEquals(WifiManager.UNKNOWN_SSID, ssid);
158         return unquoteSSID(ssid);
159     }
160 
hasCellularNetwork()161     boolean hasCellularNetwork() throws Exception {
162         if (!mHasTelephony) {
163             Log.d(TAG, "Telephony feature not found");
164             return false;
165         }
166 
167         if (isAirplaneModeOn()) {
168             // Shortcut. When mHasTelephony=true, setAirplaneMode makes sure the cellular network
169             // is connected before returning. Thus, if we turn airplane mode off and the wait
170             // succeeds, we can assume there's a cellular network.
171             setAirplaneMode(false);
172             return true;
173         }
174 
175         Network[] networks = mConnectivityManager.getAllNetworks();
176         for (Network network : networks) {
177             if (mConnectivityManager.getNetworkCapabilities(network)
178                     .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
179                 return true;
180             }
181         }
182 
183         Log.d(TAG, "Cellular network not found");
184         return false;
185     }
186 
hasEthernetConnection()187     boolean hasEthernetConnection() {
188         if (!mHasEthernet) return false;
189         Network[] networks = mConnectivityManager.getAllNetworks();
190         for (Network network : networks) {
191             NetworkCapabilities networkCapabilities =
192                     mConnectivityManager.getNetworkCapabilities(network);
193             if (networkCapabilities != null
194                     && networkCapabilities.hasTransport(TRANSPORT_ETHERNET)) {
195                 return true;
196             }
197         }
198         return false;
199     }
200 
hasWifiFeature()201     boolean hasWifiFeature() {
202         return mHasWifi;
203     }
204 
isAirplaneModeOn()205     boolean isAirplaneModeOn() throws Exception {
206         final String output = SystemUtil.runShellCommand(mInstrumentation,
207                 "cmd connectivity airplane-mode").trim();
208         return "enabled".equals(output);
209     }
210 
isDataSaverEnabled()211     boolean isDataSaverEnabled() throws Exception {
212         return SystemUtil
213                 .runShellCommand(mInstrumentation, RESTRICT_BACKGROUND_GET_CMD)
214                 .contains("enabled");
215     }
216 
isWiFiConnected()217     boolean isWiFiConnected() {
218         if (!mWifiManager.isWifiEnabled()) {
219             return false;
220         }
221         final Network network = mConnectivityManager.getActiveNetwork();
222         if (network == null) {
223             return false;
224         }
225         final NetworkCapabilities networkCapabilities =
226                 mConnectivityManager.getNetworkCapabilities(network);
227         return networkCapabilities != null
228                 && networkCapabilities.hasTransport(TRANSPORT_WIFI)
229                 && networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED);
230     }
231 
isWifiEnabled()232     boolean isWifiEnabled() {
233         return mWifiManager.isWifiEnabled();
234     }
235 
236     /**
237      * Tries to set all network statuses to {@code enabled}.
238      * However, this does not support ethernet connections.
239      * Confirm that {@link #hasEthernetConnection()} returns false before relying on this.
240      */
setAllNetworksEnabled(boolean enabled)241     void setAllNetworksEnabled(boolean enabled) throws Exception {
242         if (mHasWifi) {
243             setWifiState(enabled);
244         }
245         setAirplaneMode(!enabled);
246     }
247 
setAirplaneMode(boolean on)248     void setAirplaneMode(boolean on) throws Exception {
249         if (isAirplaneModeOn() == on) {
250             return;
251         }
252         final CallbackAsserter airplaneModeBroadcastAsserter = CallbackAsserter.forBroadcast(
253                 new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
254         SystemUtil.runShellCommand(mInstrumentation,
255                 "cmd connectivity airplane-mode " + (on ? "enable" : "disable"));
256         airplaneModeBroadcastAsserter.assertCalled("Didn't get airplane mode changed broadcast",
257                 15 /* 15 seconds */);
258         if (!on && mHasWifi) {
259             // Try to trigger some network connection.
260             setWifiState(true);
261         }
262         waitUntil("Airplane mode didn't change to " + (on ? " on" : " off"), 60 /* seconds */,
263                 () -> {
264                     // Airplane mode only affects the cellular network. If the device doesn't
265                     // support cellular, then we can only check that the airplane mode toggle is on.
266                     if (!mHasTelephony) {
267                         return on == isAirplaneModeOn();
268                     }
269                     if (on) {
270                         Network[] networks = mConnectivityManager.getAllNetworks();
271                         for (Network network : networks) {
272                             NetworkCapabilities networkCapabilities =
273                                     mConnectivityManager.getNetworkCapabilities(network);
274                             if (networkCapabilities != null && networkCapabilities
275                                     .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
276                                 return false;
277                             }
278                         }
279                         return true;
280                     } else {
281                         return mConnectivityManager.getActiveNetwork() != null;
282                     }
283                 });
284         // Wait some time for the network changes to propagate. Can't use
285         // waitUntil(isAirplaneModeOn() == on) because the response quickly gives the new
286         // airplane mode status even though the network changes haven't propagated all the way to
287         // JobScheduler.
288         Thread.sleep(5000);
289     }
290 
291     /**
292      * Sets Data Saver to the desired on/off state.
293      */
setDataSaverEnabled(boolean enabled)294     void setDataSaverEnabled(boolean enabled) throws Exception {
295         SystemUtil.runShellCommand(mInstrumentation,
296                 enabled ? RESTRICT_BACKGROUND_ON_CMD : RESTRICT_BACKGROUND_OFF_CMD);
297         final NetworkPolicyManager networkPolicyManager =
298                 mContext.getSystemService(NetworkPolicyManager.class);
299         waitUntil("Data saver " + (enabled ? "not enabled" : "still enabled"),
300                 () -> enabled == SystemUtil.runWithShellPermissionIdentity(
301                         () -> networkPolicyManager.getRestrictBackground(),
302                         Manifest.permission.MANAGE_NETWORK_POLICY));
303     }
304 
setLocationMode(String mode)305     private void setLocationMode(String mode) throws Exception {
306         Settings.Secure.putString(mContext.getContentResolver(),
307                 Settings.Secure.LOCATION_MODE, mode);
308         final LocationManager locationManager = mContext.getSystemService(LocationManager.class);
309         final boolean wantEnabled = !String.valueOf(Settings.Secure.LOCATION_MODE_OFF).equals(mode);
310         waitUntil("Location " + (wantEnabled ? "not enabled" : "still enabled"),
311                 () -> wantEnabled == locationManager.isLocationEnabled());
312     }
313 
setWifiMeteredState(boolean metered)314     void setWifiMeteredState(boolean metered) throws Exception {
315         if (metered) {
316             // Make sure unmetered cellular networks don't interfere.
317             setAirplaneMode(true);
318             setWifiState(true);
319         }
320         final String ssid = getWifiSSID();
321         setWifiMeteredState(ssid, metered ? "true" : "false");
322     }
323 
324     // metered should be "true", "false" or "none"
setWifiMeteredState(String ssid, String metered)325     private void setWifiMeteredState(String ssid, String metered) {
326         if (metered.equals(getWifiMeteredStatus(ssid))) {
327             return;
328         }
329         SystemUtil.runShellCommand("cmd netpolicy set metered-network " + ssid + " " + metered);
330         assertEquals(getWifiMeteredStatus(ssid), metered);
331     }
332 
333     /**
334      * Set Wifi connection to specific state, and block until we've verified
335      * that we are in the state.
336      * Taken from {@link android.net.http.cts.ApacheHttpClientTest}.
337      */
setWifiState(final boolean enable)338     void setWifiState(final boolean enable) throws Exception {
339         if (!mHasWifi) {
340             Log.w(TAG, "Tried to change wifi state when device doesn't have wifi feature");
341             return;
342         }
343         if (enable != isWiFiConnected()) {
344             NetworkRequest nr = new NetworkRequest.Builder().clearCapabilities().build();
345             NetworkCapabilities nc = new NetworkCapabilities.Builder()
346                     .addTransportType(TRANSPORT_WIFI)
347                     .addCapability(NET_CAPABILITY_VALIDATED)
348                     .build();
349             NetworkTracker tracker = new NetworkTracker(nc, enable, mConnectivityManager);
350             mConnectivityManager.registerNetworkCallback(nr, tracker);
351 
352             if (enable) {
353                 SystemUtil.runShellCommand("svc wifi enable");
354                 waitUntil("Failed to enable Wifi", 30 /* seconds */,
355                         this::isWifiEnabled);
356                 //noinspection deprecation
357                 SystemUtil.runWithShellPermissionIdentity(mWifiManager::reconnect,
358                         android.Manifest.permission.NETWORK_SETTINGS);
359             } else {
360                 SystemUtil.runShellCommand("svc wifi disable");
361             }
362 
363             tracker.waitForStateChange();
364 
365             assertEquals("Wifi must be " + (enable ? "connected to" : "disconnected from")
366                     + " an access point for this test.", enable, isWiFiConnected());
367 
368             mConnectivityManager.unregisterNetworkCallback(tracker);
369         }
370     }
371 
tearDown()372     void tearDown() throws Exception {
373         // Restore initial restrict background data usage policy
374         setDataSaverEnabled(mInitialDataSaverState);
375 
376         // Ensure that we leave WiFi in its previous state.
377         if (mHasWifi) {
378             if (mInitialWiFiSSID != null) {
379                 setWifiMeteredState(mInitialWiFiSSID, mInitialWiFiMeteredState);
380             }
381             if (mWifiManager.isWifiEnabled() != mInitialWiFiState) {
382                 try {
383                     setWifiState(mInitialWiFiState);
384                 } catch (AssertionError e) {
385                     // Don't fail the test just because wifi state wasn't set in tearDown.
386                     Log.e(TAG, "Failed to return wifi state to " + mInitialWiFiState, e);
387                 }
388             }
389         }
390 
391         // Restore initial airplane mode status. Do it after setting wifi in case wifi was
392         // originally metered.
393         if (isAirplaneModeOn() != mInitialAirplaneModeState) {
394             setAirplaneMode(mInitialAirplaneModeState);
395         }
396 
397         setLocationMode(mInitialLocationMode);
398     }
399 
400     @Override
close()401     public void close() throws Exception {
402         tearDown();
403     }
404 
unquoteSSID(String ssid)405     private String unquoteSSID(String ssid) {
406         // SSID is returned surrounded by quotes if it can be decoded as UTF-8.
407         // Otherwise it's guaranteed not to start with a quote.
408         if (ssid.charAt(0) == '"') {
409             return ssid.substring(1, ssid.length() - 1);
410         } else {
411             return ssid;
412         }
413     }
414 
415     static class NetworkTracker extends ConnectivityManager.NetworkCallback {
416         private static final int MSG_CHECK_ACTIVE_NETWORK = 1;
417         private final ConnectivityManager mConnectivityManager;
418 
419         private final CountDownLatch mReceiveLatch = new CountDownLatch(1);
420 
421         private final NetworkCapabilities mExpectedCapabilities;
422 
423         private final boolean mExpectedConnected;
424 
425         private final Handler mHandler = new Handler(Looper.getMainLooper()) {
426             @Override
427             public void handleMessage(Message msg) {
428                 if (msg.what == MSG_CHECK_ACTIVE_NETWORK) {
429                     checkActiveNetwork();
430                 }
431             }
432         };
433 
NetworkTracker(NetworkCapabilities expectedCapabilities, boolean expectedConnected, ConnectivityManager cm)434         NetworkTracker(NetworkCapabilities expectedCapabilities, boolean expectedConnected,
435                 ConnectivityManager cm) {
436             mExpectedCapabilities = expectedCapabilities;
437             mExpectedConnected = expectedConnected;
438             mConnectivityManager = cm;
439         }
440 
441         @Override
onAvailable(Network network)442         public void onAvailable(Network network) {
443             // Available doesn't mean it's the active network. We need to check that separately.
444             checkActiveNetwork();
445         }
446 
447         @Override
onLost(Network network)448         public void onLost(Network network) {
449             checkActiveNetwork();
450         }
451 
waitForStateChange()452         boolean waitForStateChange() throws InterruptedException {
453             checkActiveNetwork();
454             return mReceiveLatch.await(60, TimeUnit.SECONDS);
455         }
456 
checkActiveNetwork()457         private void checkActiveNetwork() {
458             mHandler.removeMessages(MSG_CHECK_ACTIVE_NETWORK);
459             if (mReceiveLatch.getCount() == 0) {
460                 return;
461             }
462 
463             Network activeNetwork = mConnectivityManager.getActiveNetwork();
464             if (mExpectedConnected) {
465                 if (activeNetwork != null && mExpectedCapabilities.satisfiedByNetworkCapabilities(
466                         mConnectivityManager.getNetworkCapabilities(activeNetwork))) {
467                     mReceiveLatch.countDown();
468                 } else {
469                     mHandler.sendEmptyMessageDelayed(MSG_CHECK_ACTIVE_NETWORK, 5000);
470                 }
471             } else {
472                 if (activeNetwork == null
473                         || !mExpectedCapabilities.satisfiedByNetworkCapabilities(
474                         mConnectivityManager.getNetworkCapabilities(activeNetwork))) {
475                     mReceiveLatch.countDown();
476                 } else {
477                     mHandler.sendEmptyMessageDelayed(MSG_CHECK_ACTIVE_NETWORK, 5000);
478                 }
479             }
480         }
481     }
482 }
483