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