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