| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.jobscheduler.cts; |
| |
| import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; |
| import static android.net.NetworkCapabilities.TRANSPORT_WIFI; |
| |
| import static com.android.compatibility.common.util.TestUtils.waitUntil; |
| |
| import static junit.framework.Assert.fail; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| |
| import android.Manifest; |
| import android.annotation.NonNull; |
| import android.app.Instrumentation; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageManager; |
| import android.location.LocationManager; |
| import android.net.ConnectivityManager; |
| import android.net.Network; |
| import android.net.NetworkCapabilities; |
| import android.net.NetworkRequest; |
| import android.net.wifi.WifiConfiguration; |
| import android.net.wifi.WifiManager; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.provider.Settings; |
| import android.util.Log; |
| |
| import com.android.compatibility.common.util.CallbackAsserter; |
| import com.android.compatibility.common.util.ShellIdentityUtils; |
| import com.android.compatibility.common.util.SystemUtil; |
| |
| import junit.framework.AssertionFailedError; |
| |
| import java.util.List; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| public class NetworkingHelper { |
| private static final String TAG = "JsNetworkingUtils"; |
| |
| private static final String RESTRICT_BACKGROUND_GET_CMD = |
| "cmd netpolicy get restrict-background"; |
| private static final String RESTRICT_BACKGROUND_ON_CMD = |
| "cmd netpolicy set restrict-background true"; |
| private static final String RESTRICT_BACKGROUND_OFF_CMD = |
| "cmd netpolicy set restrict-background false"; |
| |
| private final Context mContext; |
| private final Instrumentation mInstrumentation; |
| |
| private final ConnectivityManager mConnectivityManager; |
| private final WifiManager mWifiManager; |
| |
| /** Whether the device running these tests supports WiFi. */ |
| private final boolean mHasWifi; |
| /** Whether the device running these tests supports ethernet. */ |
| private final boolean mHasEthernet; |
| /** Whether the device running these tests supports telephony. */ |
| private final boolean mHasTelephony; |
| |
| private final boolean mInitialAirplaneModeState; |
| private final boolean mInitialDataSaverState; |
| private final String mInitialLocationMode; |
| private final boolean mInitialWiFiState; |
| private String mInitialWiFiMeteredState; |
| private String mInitialWiFiSSID; |
| |
| NetworkingHelper(@NonNull Instrumentation instrumentation, @NonNull Context context) |
| throws Exception { |
| mContext = context; |
| mInstrumentation = instrumentation; |
| |
| mConnectivityManager = context.getSystemService(ConnectivityManager.class); |
| mWifiManager = context.getSystemService(WifiManager.class); |
| |
| PackageManager packageManager = mContext.getPackageManager(); |
| mHasWifi = packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI); |
| mHasEthernet = packageManager.hasSystemFeature(PackageManager.FEATURE_ETHERNET); |
| mHasTelephony = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY); |
| |
| mInitialAirplaneModeState = isAirplaneModeOn(); |
| mInitialDataSaverState = isDataSaverEnabled(); |
| mInitialLocationMode = Settings.Secure.getString( |
| mContext.getContentResolver(), Settings.Secure.LOCATION_MODE); |
| mInitialWiFiState = mHasWifi && isWifiEnabled(); |
| } |
| |
| /** Ensures that the device has a wifi network saved. */ |
| void ensureSavedWifiNetwork() throws Exception { |
| if (!mHasWifi) { |
| return; |
| } |
| final List<WifiConfiguration> savedNetworks = |
| ShellIdentityUtils.invokeMethodWithShellPermissions( |
| mWifiManager, WifiManager::getConfiguredNetworks); |
| assertFalse("Need at least one saved wifi network", savedNetworks.isEmpty()); |
| |
| setWifiState(true); |
| if (mInitialWiFiSSID == null) { |
| mInitialWiFiSSID = getWifiSSID(); |
| mInitialWiFiMeteredState = getWifiMeteredStatus(mInitialWiFiSSID); |
| } |
| } |
| |
| // Returns "true", "false", or "none". |
| private String getWifiMeteredStatus(String ssid) { |
| // Interestingly giving the SSID as an argument to list wifi-networks |
| // only works iff the network in question has the "false" policy. |
| // Also unfortunately runShellCommand does not pass the command to the interpreter |
| // so it's not possible to | grep the ssid. |
| final String command = "cmd netpolicy list wifi-networks"; |
| final String policyString = SystemUtil.runShellCommand(command); |
| |
| final Matcher m = Pattern.compile(ssid + ";(true|false|none)", |
| Pattern.MULTILINE | Pattern.UNIX_LINES).matcher(policyString); |
| if (!m.find()) { |
| fail("Unexpected format from cmd netpolicy (when looking for " + ssid + "): " |
| + policyString); |
| } |
| return m.group(1); |
| } |
| |
| @NonNull |
| private String getWifiSSID() throws Exception { |
| // Location needs to be enabled to get the WiFi information. |
| setLocationMode(String.valueOf(Settings.Secure.LOCATION_MODE_ON)); |
| final AtomicReference<String> ssid = new AtomicReference<>(); |
| SystemUtil.runWithShellPermissionIdentity( |
| () -> ssid.set(mWifiManager.getConnectionInfo().getSSID()), |
| Manifest.permission.ACCESS_FINE_LOCATION); |
| return unquoteSSID(ssid.get()); |
| } |
| |
| boolean hasEthernetConnection() { |
| if (!mHasEthernet) return false; |
| Network[] networks = mConnectivityManager.getAllNetworks(); |
| for (Network network : networks) { |
| if (mConnectivityManager.getNetworkCapabilities(network) |
| .hasTransport(TRANSPORT_ETHERNET)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| boolean hasWifiFeature() { |
| return mHasWifi; |
| } |
| |
| boolean isAirplaneModeOn() throws Exception { |
| final String output = SystemUtil.runShellCommand(mInstrumentation, |
| "cmd connectivity airplane-mode").trim(); |
| return "enabled".equals(output); |
| } |
| |
| boolean isDataSaverEnabled() throws Exception { |
| return SystemUtil |
| .runShellCommand(mInstrumentation, RESTRICT_BACKGROUND_GET_CMD) |
| .contains("enabled"); |
| } |
| |
| boolean isWiFiConnected() { |
| if (!mWifiManager.isWifiEnabled()) { |
| return false; |
| } |
| final Network network = mConnectivityManager.getActiveNetwork(); |
| if (network == null) { |
| return false; |
| } |
| final NetworkCapabilities networkCapabilities = |
| mConnectivityManager.getNetworkCapabilities(network); |
| return networkCapabilities != null && networkCapabilities.hasTransport(TRANSPORT_WIFI); |
| } |
| |
| boolean isWifiEnabled() { |
| return mWifiManager.isWifiEnabled(); |
| } |
| |
| /** |
| * Tries to set all network statuses to {@code enabled}. |
| * However, this does not support ethernet connections. |
| * Confirm that {@link #hasEthernetConnection()} returns false before relying on this. |
| */ |
| void setAllNetworksEnabled(boolean enabled) throws Exception { |
| if (mHasWifi) { |
| setWifiState(enabled); |
| } |
| setAirplaneMode(!enabled); |
| } |
| |
| void setAirplaneMode(boolean on) throws Exception { |
| if (isAirplaneModeOn() == on) { |
| return; |
| } |
| final CallbackAsserter airplaneModeBroadcastAsserter = CallbackAsserter.forBroadcast( |
| new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)); |
| SystemUtil.runShellCommand(mInstrumentation, |
| "cmd connectivity airplane-mode " + (on ? "enable" : "disable")); |
| airplaneModeBroadcastAsserter.assertCalled("Didn't get airplane mode changed broadcast", |
| 15 /* 15 seconds */); |
| if (!on && mHasWifi) { |
| // Try to trigger some network connection. |
| setWifiState(true); |
| } |
| waitUntil("Airplane mode didn't change to " + (on ? " on" : " off"), 60 /* seconds */, |
| () -> { |
| // Airplane mode only affects the cellular network. If the device doesn't |
| // support cellular, then we can only check that the airplane mode toggle is on. |
| if (!mHasTelephony) { |
| return on == isAirplaneModeOn(); |
| } |
| if (on) { |
| Network[] networks = mConnectivityManager.getAllNetworks(); |
| for (Network network : networks) { |
| if (mConnectivityManager.getNetworkCapabilities(network) |
| .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { |
| return false; |
| } |
| } |
| return true; |
| } else { |
| return mConnectivityManager.getActiveNetwork() != null; |
| } |
| }); |
| // Wait some time for the network changes to propagate. Can't use |
| // waitUntil(isAirplaneModeOn() == on) because the response quickly gives the new |
| // airplane mode status even though the network changes haven't propagated all the way to |
| // JobScheduler. |
| Thread.sleep(5000); |
| } |
| |
| /** |
| * Ensures that restrict background data usage policy is turned off. |
| * If the policy is on, it interferes with tests that relies on metered connection. |
| */ |
| void setDataSaverEnabled(boolean enabled) throws Exception { |
| SystemUtil.runShellCommand(mInstrumentation, |
| enabled ? RESTRICT_BACKGROUND_ON_CMD : RESTRICT_BACKGROUND_OFF_CMD); |
| } |
| |
| private void setLocationMode(String mode) throws Exception { |
| Settings.Secure.putString(mContext.getContentResolver(), |
| Settings.Secure.LOCATION_MODE, mode); |
| final LocationManager locationManager = mContext.getSystemService(LocationManager.class); |
| final boolean wantEnabled = !String.valueOf(Settings.Secure.LOCATION_MODE_OFF).equals(mode); |
| waitUntil("Location " + (wantEnabled ? "not enabled" : "still enabled"), |
| () -> wantEnabled == locationManager.isLocationEnabled()); |
| } |
| |
| void setWifiMeteredState(boolean metered) throws Exception { |
| if (metered) { |
| // Make sure unmetered cellular networks don't interfere. |
| setAirplaneMode(true); |
| setWifiState(true); |
| } |
| final String ssid = getWifiSSID(); |
| setWifiMeteredState(ssid, metered ? "true" : "false"); |
| } |
| |
| // metered should be "true", "false" or "none" |
| private void setWifiMeteredState(String ssid, String metered) { |
| if (metered.equals(getWifiMeteredStatus(ssid))) { |
| return; |
| } |
| SystemUtil.runShellCommand("cmd netpolicy set metered-network " + ssid + " " + metered); |
| assertEquals(getWifiMeteredStatus(ssid), metered); |
| } |
| |
| /** |
| * Set Wifi connection to specific state, and block until we've verified |
| * that we are in the state. |
| * Taken from {@link android.net.http.cts.ApacheHttpClientTest}. |
| */ |
| void setWifiState(final boolean enable) throws Exception { |
| if (enable != isWiFiConnected()) { |
| NetworkRequest nr = new NetworkRequest.Builder().clearCapabilities().build(); |
| NetworkCapabilities nc = new NetworkCapabilities.Builder() |
| .addTransportType(TRANSPORT_WIFI) |
| .build(); |
| NetworkTracker tracker = new NetworkTracker(nc, enable, mConnectivityManager); |
| mConnectivityManager.registerNetworkCallback(nr, tracker); |
| |
| if (enable) { |
| SystemUtil.runShellCommand("svc wifi enable"); |
| waitUntil("Failed to enable Wifi", 30 /* seconds */, |
| this::isWifiEnabled); |
| //noinspection deprecation |
| SystemUtil.runWithShellPermissionIdentity(mWifiManager::reconnect, |
| android.Manifest.permission.NETWORK_SETTINGS); |
| } else { |
| SystemUtil.runShellCommand("svc wifi disable"); |
| } |
| |
| tracker.waitForStateChange(); |
| |
| assertEquals("Wifi must be " + (enable ? "connected to" : "disconnected from") |
| + " an access point for this test.", enable, isWiFiConnected()); |
| |
| mConnectivityManager.unregisterNetworkCallback(tracker); |
| } |
| } |
| |
| void tearDown() throws Exception { |
| // Restore initial restrict background data usage policy |
| setDataSaverEnabled(mInitialDataSaverState); |
| |
| // Ensure that we leave WiFi in its previous state. |
| if (mHasWifi) { |
| if (mInitialWiFiSSID != null) { |
| setWifiMeteredState(mInitialWiFiSSID, mInitialWiFiMeteredState); |
| } |
| if (mWifiManager.isWifiEnabled() != mInitialWiFiState) { |
| try { |
| setWifiState(mInitialWiFiState); |
| } catch (AssertionFailedError e) { |
| // Don't fail the test just because wifi state wasn't set in tearDown. |
| Log.e(TAG, "Failed to return wifi state to " + mInitialWiFiState, e); |
| } |
| } |
| } |
| |
| // Restore initial airplane mode status. Do it after setting wifi in case wifi was |
| // originally metered. |
| if (isAirplaneModeOn() != mInitialAirplaneModeState) { |
| setAirplaneMode(mInitialAirplaneModeState); |
| } |
| |
| setLocationMode(mInitialLocationMode); |
| } |
| |
| private String unquoteSSID(String ssid) { |
| // SSID is returned surrounded by quotes if it can be decoded as UTF-8. |
| // Otherwise it's guaranteed not to start with a quote. |
| if (ssid.charAt(0) == '"') { |
| return ssid.substring(1, ssid.length() - 1); |
| } else { |
| return ssid; |
| } |
| } |
| |
| static class NetworkTracker extends ConnectivityManager.NetworkCallback { |
| private static final int MSG_CHECK_ACTIVE_NETWORK = 1; |
| private final ConnectivityManager mConnectivityManager; |
| |
| private final CountDownLatch mReceiveLatch = new CountDownLatch(1); |
| |
| private final NetworkCapabilities mExpectedCapabilities; |
| |
| private final boolean mExpectedConnected; |
| |
| private final Handler mHandler = new Handler(Looper.getMainLooper()) { |
| @Override |
| public void handleMessage(Message msg) { |
| if (msg.what == MSG_CHECK_ACTIVE_NETWORK) { |
| checkActiveNetwork(); |
| } |
| } |
| }; |
| |
| NetworkTracker(NetworkCapabilities expectedCapabilities, boolean expectedConnected, |
| ConnectivityManager cm) { |
| mExpectedCapabilities = expectedCapabilities; |
| mExpectedConnected = expectedConnected; |
| mConnectivityManager = cm; |
| } |
| |
| @Override |
| public void onAvailable(Network network) { |
| // Available doesn't mean it's the active network. We need to check that separately. |
| checkActiveNetwork(); |
| } |
| |
| @Override |
| public void onLost(Network network) { |
| checkActiveNetwork(); |
| } |
| |
| boolean waitForStateChange() throws InterruptedException { |
| checkActiveNetwork(); |
| return mReceiveLatch.await(60, TimeUnit.SECONDS); |
| } |
| |
| private void checkActiveNetwork() { |
| mHandler.removeMessages(MSG_CHECK_ACTIVE_NETWORK); |
| if (mReceiveLatch.getCount() == 0) { |
| return; |
| } |
| |
| Network activeNetwork = mConnectivityManager.getActiveNetwork(); |
| if (mExpectedConnected) { |
| if (activeNetwork != null && mExpectedCapabilities.satisfiedByNetworkCapabilities( |
| mConnectivityManager.getNetworkCapabilities(activeNetwork))) { |
| mReceiveLatch.countDown(); |
| } else { |
| mHandler.sendEmptyMessageDelayed(MSG_CHECK_ACTIVE_NETWORK, 5000); |
| } |
| } else { |
| if (activeNetwork == null |
| || !mExpectedCapabilities.satisfiedByNetworkCapabilities( |
| mConnectivityManager.getNetworkCapabilities(activeNetwork))) { |
| mReceiveLatch.countDown(); |
| } else { |
| mHandler.sendEmptyMessageDelayed(MSG_CHECK_ACTIVE_NETWORK, 5000); |
| } |
| } |
| } |
| } |
| } |