diff options
Diffstat (limited to 'tests/JobScheduler/src/android/jobscheduler/cts/NetworkingHelper.java')
-rw-r--r-- | tests/JobScheduler/src/android/jobscheduler/cts/NetworkingHelper.java | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/NetworkingHelper.java b/tests/JobScheduler/src/android/jobscheduler/cts/NetworkingHelper.java new file mode 100644 index 00000000000..1402975cfe6 --- /dev/null +++ b/tests/JobScheduler/src/android/jobscheduler/cts/NetworkingHelper.java @@ -0,0 +1,432 @@ +/* + * 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); + } + } + } + } +} |