blob: 1402975cfe62d813a8f4cadc8c34395915d8260d [file] [log] [blame]
/*
* 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);
}
}
}
}
}