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