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