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         assertNotEquals(WifiManager.UNKNOWN_SSID, ssid);
157         return unquoteSSID(ssid);
158     }
159 
hasCellularNetwork()160     boolean hasCellularNetwork() throws Exception {
161         if (!mHasTelephony) {
162             Log.d(TAG, "Telephony feature not found");
163             return false;
164         }
165 
166         if (isAirplaneModeOn()) {
167             // Shortcut. When mHasTelephony=true, setAirplaneMode makes sure the cellular network
168             // is connected before returning. Thus, if we turn airplane mode off and the wait
169             // succeeds, we can assume there's a cellular network.
170             setAirplaneMode(false);
171             return true;
172         }
173 
174         Network[] networks = mConnectivityManager.getAllNetworks();
175         for (Network network : networks) {
176             if (mConnectivityManager.getNetworkCapabilities(network)
177                     .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
178                 return true;
179             }
180         }
181 
182         Log.d(TAG, "Cellular network not found");
183         return false;
184     }
185 
hasEthernetConnection()186     boolean hasEthernetConnection() {
187         if (!mHasEthernet) return false;
188         Network[] networks = mConnectivityManager.getAllNetworks();
189         for (Network network : networks) {
190             NetworkCapabilities networkCapabilities =
191                     mConnectivityManager.getNetworkCapabilities(network);
192             if (networkCapabilities != null
193                     && networkCapabilities.hasTransport(TRANSPORT_ETHERNET)) {
194                 return true;
195             }
196         }
197         return false;
198     }
199 
hasWifiFeature()200     boolean hasWifiFeature() {
201         return mHasWifi;
202     }
203 
isAirplaneModeOn()204     boolean isAirplaneModeOn() throws Exception {
205         final String output = SystemUtil.runShellCommand(mInstrumentation,
206                 "cmd connectivity airplane-mode").trim();
207         return "enabled".equals(output);
208     }
209 
isDataSaverEnabled()210     boolean isDataSaverEnabled() throws Exception {
211         return SystemUtil
212                 .runShellCommand(mInstrumentation, RESTRICT_BACKGROUND_GET_CMD)
213                 .contains("enabled");
214     }
215 
isWiFiConnected()216     boolean isWiFiConnected() {
217         if (!mWifiManager.isWifiEnabled()) {
218             return false;
219         }
220         final Network network = mConnectivityManager.getActiveNetwork();
221         if (network == null) {
222             return false;
223         }
224         final NetworkCapabilities networkCapabilities =
225                 mConnectivityManager.getNetworkCapabilities(network);
226         return networkCapabilities != null
227                 && networkCapabilities.hasTransport(TRANSPORT_WIFI)
228                 && networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED);
229     }
230 
isWifiEnabled()231     boolean isWifiEnabled() {
232         return mWifiManager.isWifiEnabled();
233     }
234 
235     /**
236      * Tries to set all network statuses to {@code enabled}.
237      * However, this does not support ethernet connections.
238      * Confirm that {@link #hasEthernetConnection()} returns false before relying on this.
239      */
setAllNetworksEnabled(boolean enabled)240     void setAllNetworksEnabled(boolean enabled) throws Exception {
241         if (mHasWifi) {
242             setWifiState(enabled);
243         }
244         setAirplaneMode(!enabled);
245     }
246 
setAirplaneMode(boolean on)247     void setAirplaneMode(boolean on) throws Exception {
248         if (isAirplaneModeOn() == on) {
249             return;
250         }
251         final CallbackAsserter airplaneModeBroadcastAsserter = CallbackAsserter.forBroadcast(
252                 new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
253         SystemUtil.runShellCommand(mInstrumentation,
254                 "cmd connectivity airplane-mode " + (on ? "enable" : "disable"));
255         airplaneModeBroadcastAsserter.assertCalled("Didn't get airplane mode changed broadcast",
256                 15 /* 15 seconds */);
257         if (!on && mHasWifi) {
258             // Try to trigger some network connection.
259             setWifiState(true);
260         }
261         waitUntil("Airplane mode didn't change to " + (on ? " on" : " off"), 60 /* seconds */,
262                 () -> {
263                     // Airplane mode only affects the cellular network. If the device doesn't
264                     // support cellular, then we can only check that the airplane mode toggle is on.
265                     if (!mHasTelephony) {
266                         return on == isAirplaneModeOn();
267                     }
268                     if (on) {
269                         Network[] networks = mConnectivityManager.getAllNetworks();
270                         for (Network network : networks) {
271                             NetworkCapabilities networkCapabilities =
272                                     mConnectivityManager.getNetworkCapabilities(network);
273                             if (networkCapabilities != null && networkCapabilities
274                                     .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
275                                 return false;
276                             }
277                         }
278                         return true;
279                     } else {
280                         return mConnectivityManager.getActiveNetwork() != null;
281                     }
282                 });
283         // Wait some time for the network changes to propagate. Can't use
284         // waitUntil(isAirplaneModeOn() == on) because the response quickly gives the new
285         // airplane mode status even though the network changes haven't propagated all the way to
286         // JobScheduler.
287         Thread.sleep(5000);
288     }
289 
290     /**
291      * Sets Data Saver to the desired on/off state.
292      */
setDataSaverEnabled(boolean enabled)293     void setDataSaverEnabled(boolean enabled) throws Exception {
294         SystemUtil.runShellCommand(mInstrumentation,
295                 enabled ? RESTRICT_BACKGROUND_ON_CMD : RESTRICT_BACKGROUND_OFF_CMD);
296         final NetworkPolicyManager networkPolicyManager =
297                 mContext.getSystemService(NetworkPolicyManager.class);
298         waitUntil("Data saver " + (enabled ? "not enabled" : "still enabled"),
299                 () -> enabled == SystemUtil.runWithShellPermissionIdentity(
300                         () -> networkPolicyManager.getRestrictBackground(),
301                         Manifest.permission.MANAGE_NETWORK_POLICY));
302     }
303 
setLocationMode(String mode)304     private void setLocationMode(String mode) throws Exception {
305         Settings.Secure.putString(mContext.getContentResolver(),
306                 Settings.Secure.LOCATION_MODE, mode);
307         final LocationManager locationManager = mContext.getSystemService(LocationManager.class);
308         final boolean wantEnabled = !String.valueOf(Settings.Secure.LOCATION_MODE_OFF).equals(mode);
309         waitUntil("Location " + (wantEnabled ? "not enabled" : "still enabled"),
310                 () -> wantEnabled == locationManager.isLocationEnabled());
311     }
312 
setWifiMeteredState(boolean metered)313     void setWifiMeteredState(boolean metered) throws Exception {
314         if (metered) {
315             // Make sure unmetered cellular networks don't interfere.
316             setAirplaneMode(true);
317             setWifiState(true);
318         }
319         final String ssid = getWifiSSID();
320         setWifiMeteredState(ssid, metered ? "true" : "false");
321     }
322 
323     // metered should be "true", "false" or "none"
setWifiMeteredState(String ssid, String metered)324     private void setWifiMeteredState(String ssid, String metered) {
325         if (metered.equals(getWifiMeteredStatus(ssid))) {
326             return;
327         }
328         SystemUtil.runShellCommand("cmd netpolicy set metered-network " + ssid + " " + metered);
329         assertEquals(getWifiMeteredStatus(ssid), metered);
330     }
331 
332     /**
333      * Set Wifi connection to specific state, and block until we've verified
334      * that we are in the state.
335      * Taken from {@link android.net.http.cts.ApacheHttpClientTest}.
336      */
setWifiState(final boolean enable)337     void setWifiState(final boolean enable) throws Exception {
338         if (!mHasWifi) {
339             Log.w(TAG, "Tried to change wifi state when device doesn't have wifi feature");
340             return;
341         }
342         if (enable != isWiFiConnected()) {
343             NetworkRequest nr = new NetworkRequest.Builder().clearCapabilities().build();
344             NetworkCapabilities nc = new NetworkCapabilities.Builder()
345                     .addTransportType(TRANSPORT_WIFI)
346                     .addCapability(NET_CAPABILITY_VALIDATED)
347                     .build();
348             NetworkTracker tracker = new NetworkTracker(nc, enable, mConnectivityManager);
349             mConnectivityManager.registerNetworkCallback(nr, tracker);
350 
351             if (enable) {
352                 SystemUtil.runShellCommand("svc wifi enable");
353                 waitUntil("Failed to enable Wifi", 30 /* seconds */,
354                         this::isWifiEnabled);
355                 //noinspection deprecation
356                 SystemUtil.runWithShellPermissionIdentity(mWifiManager::reconnect,
357                         android.Manifest.permission.NETWORK_SETTINGS);
358             } else {
359                 SystemUtil.runShellCommand("svc wifi disable");
360             }
361 
362             tracker.waitForStateChange();
363 
364             assertEquals("Wifi must be " + (enable ? "connected to" : "disconnected from")
365                     + " an access point for this test.", enable, isWiFiConnected());
366 
367             mConnectivityManager.unregisterNetworkCallback(tracker);
368         }
369     }
370 
tearDown()371     void tearDown() throws Exception {
372         // Restore initial restrict background data usage policy
373         setDataSaverEnabled(mInitialDataSaverState);
374 
375         // Ensure that we leave WiFi in its previous state.
376         if (mHasWifi) {
377             if (mInitialWiFiSSID != null) {
378                 setWifiMeteredState(mInitialWiFiSSID, mInitialWiFiMeteredState);
379             }
380             if (mWifiManager.isWifiEnabled() != mInitialWiFiState) {
381                 try {
382                     setWifiState(mInitialWiFiState);
383                 } catch (AssertionError e) {
384                     // Don't fail the test just because wifi state wasn't set in tearDown.
385                     Log.e(TAG, "Failed to return wifi state to " + mInitialWiFiState, e);
386                 }
387             }
388         }
389 
390         // Restore initial airplane mode status. Do it after setting wifi in case wifi was
391         // originally metered.
392         if (isAirplaneModeOn() != mInitialAirplaneModeState) {
393             setAirplaneMode(mInitialAirplaneModeState);
394         }
395 
396         setLocationMode(mInitialLocationMode);
397     }
398 
399     @Override
close()400     public void close() throws Exception {
401         tearDown();
402     }
403 
unquoteSSID(String ssid)404     private String unquoteSSID(String ssid) {
405         // SSID is returned surrounded by quotes if it can be decoded as UTF-8.
406         // Otherwise it's guaranteed not to start with a quote.
407         if (ssid.charAt(0) == '"') {
408             return ssid.substring(1, ssid.length() - 1);
409         } else {
410             return ssid;
411         }
412     }
413 
414     static class NetworkTracker extends ConnectivityManager.NetworkCallback {
415         private static final int MSG_CHECK_ACTIVE_NETWORK = 1;
416         private final ConnectivityManager mConnectivityManager;
417 
418         private final CountDownLatch mReceiveLatch = new CountDownLatch(1);
419 
420         private final NetworkCapabilities mExpectedCapabilities;
421 
422         private final boolean mExpectedConnected;
423 
424         private final Handler mHandler = new Handler(Looper.getMainLooper()) {
425             @Override
426             public void handleMessage(Message msg) {
427                 if (msg.what == MSG_CHECK_ACTIVE_NETWORK) {
428                     checkActiveNetwork();
429                 }
430             }
431         };
432 
NetworkTracker(NetworkCapabilities expectedCapabilities, boolean expectedConnected, ConnectivityManager cm)433         NetworkTracker(NetworkCapabilities expectedCapabilities, boolean expectedConnected,
434                 ConnectivityManager cm) {
435             mExpectedCapabilities = expectedCapabilities;
436             mExpectedConnected = expectedConnected;
437             mConnectivityManager = cm;
438         }
439 
440         @Override
onAvailable(Network network)441         public void onAvailable(Network network) {
442             // Available doesn't mean it's the active network. We need to check that separately.
443             checkActiveNetwork();
444         }
445 
446         @Override
onLost(Network network)447         public void onLost(Network network) {
448             checkActiveNetwork();
449         }
450 
waitForStateChange()451         boolean waitForStateChange() throws InterruptedException {
452             checkActiveNetwork();
453             return mReceiveLatch.await(60, TimeUnit.SECONDS);
454         }
455 
checkActiveNetwork()456         private void checkActiveNetwork() {
457             mHandler.removeMessages(MSG_CHECK_ACTIVE_NETWORK);
458             if (mReceiveLatch.getCount() == 0) {
459                 return;
460             }
461 
462             Network activeNetwork = mConnectivityManager.getActiveNetwork();
463             if (mExpectedConnected) {
464                 if (activeNetwork != null && mExpectedCapabilities.satisfiedByNetworkCapabilities(
465                         mConnectivityManager.getNetworkCapabilities(activeNetwork))) {
466                     mReceiveLatch.countDown();
467                 } else {
468                     mHandler.sendEmptyMessageDelayed(MSG_CHECK_ACTIVE_NETWORK, 5000);
469                 }
470             } else {
471                 if (activeNetwork == null
472                         || !mExpectedCapabilities.satisfiedByNetworkCapabilities(
473                         mConnectivityManager.getNetworkCapabilities(activeNetwork))) {
474                     mReceiveLatch.countDown();
475                 } else {
476                     mHandler.sendEmptyMessageDelayed(MSG_CHECK_ACTIVE_NETWORK, 5000);
477                 }
478             }
479         }
480     }
481 }
482