blob: 9d1d4188ca54835e528b97c6ea256fd03e23dd35 [file] [log] [blame]
/*
* Copyright (C) 2019 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 com.android.cts.net.hostside;
import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_METERED;
import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_NONE;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.app.ActivityManager;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.content.Context;
import android.location.LocationManager;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.ActionListener;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.UserHandle;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.data.ApnSetting;
import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.compatibility.common.util.AppStandbyUtils;
import com.android.compatibility.common.util.BatteryUtils;
import com.android.compatibility.common.util.ShellIdentityUtils;
import com.android.compatibility.common.util.ThrowingRunnable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class NetworkPolicyTestUtils {
// android.telephony.CarrierConfigManager.KEY_CARRIER_METERED_APN_TYPES_STRINGS
// TODO: Expose it as a @TestApi instead of copying the constant
private static final String KEY_CARRIER_METERED_APN_TYPES_STRINGS =
"carrier_metered_apn_types_strings";
private static final int TIMEOUT_CHANGE_METEREDNESS_MS = 10_000;
private static ConnectivityManager mCm;
private static WifiManager mWm;
private static CarrierConfigManager mCarrierConfigManager;
private static Boolean mBatterySaverSupported;
private static Boolean mDataSaverSupported;
private static Boolean mDozeModeSupported;
private static Boolean mAppStandbySupported;
private NetworkPolicyTestUtils() {}
public static boolean isBatterySaverSupported() {
if (mBatterySaverSupported == null) {
mBatterySaverSupported = BatteryUtils.isBatterySaverSupported();
}
return mBatterySaverSupported;
}
/**
* As per CDD requirements, if the device doesn't support data saver mode then
* ConnectivityManager.getRestrictBackgroundStatus() will always return
* RESTRICT_BACKGROUND_STATUS_DISABLED. So, enable the data saver mode and check if
* ConnectivityManager.getRestrictBackgroundStatus() for an app in background returns
* RESTRICT_BACKGROUND_STATUS_DISABLED or not.
*/
public static boolean isDataSaverSupported() {
if (mDataSaverSupported == null) {
assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED);
try {
setRestrictBackgroundInternal(true);
mDataSaverSupported = !isMyRestrictBackgroundStatus(
RESTRICT_BACKGROUND_STATUS_DISABLED);
} finally {
setRestrictBackgroundInternal(false);
}
}
return mDataSaverSupported;
}
public static boolean isDozeModeSupported() {
if (mDozeModeSupported == null) {
final String result = executeShellCommand("cmd deviceidle enabled deep");
mDozeModeSupported = result.equals("1");
}
return mDozeModeSupported;
}
public static boolean isAppStandbySupported() {
if (mAppStandbySupported == null) {
mAppStandbySupported = AppStandbyUtils.isAppStandbyEnabled();
}
return mAppStandbySupported;
}
public static boolean isLowRamDevice() {
final ActivityManager am = (ActivityManager) getContext().getSystemService(
Context.ACTIVITY_SERVICE);
return am.isLowRamDevice();
}
/** Forces JobScheduler to run the job if constraints are met. */
public static void forceRunJob(String pkg, int jobId) {
executeShellCommand("cmd jobscheduler run -f -u " + UserHandle.myUserId()
+ " " + pkg + " " + jobId);
}
public static boolean isLocationEnabled() {
final LocationManager lm = (LocationManager) getContext().getSystemService(
Context.LOCATION_SERVICE);
return lm.isLocationEnabled();
}
public static void setLocationEnabled(boolean enabled) {
final LocationManager lm = (LocationManager) getContext().getSystemService(
Context.LOCATION_SERVICE);
lm.setLocationEnabledForUser(enabled, Process.myUserHandle());
assertEquals("Couldn't change location enabled state", lm.isLocationEnabled(), enabled);
Log.d(TAG, "Changed location enabled state to " + enabled);
}
public static boolean isActiveNetworkMetered(boolean metered) {
return getConnectivityManager().isActiveNetworkMetered() == metered;
}
public static boolean canChangeActiveNetworkMeteredness() {
final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
return networkCapabilities.hasTransport(TRANSPORT_WIFI)
|| networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
}
/**
* Updates the meteredness of the active network. Right now we can only change meteredness
* of either Wifi or cellular network, so if the active network is not either of these, this
* will throw an exception.
*
* @return a {@link ThrowingRunnable} object that can used to reset the meteredness change
* made by this method.
*/
public static ThrowingRunnable setupActiveNetworkMeteredness(boolean metered) throws Exception {
if (isActiveNetworkMetered(metered)) {
return null;
}
final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
if (networkCapabilities.hasTransport(TRANSPORT_WIFI)) {
final String ssid = getWifiSsid();
setWifiMeteredStatus(ssid, metered);
return () -> setWifiMeteredStatus(ssid, !metered);
} else if (networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
final int subId = SubscriptionManager.getActiveDataSubscriptionId();
setCellularMeteredStatus(subId, metered);
return () -> setCellularMeteredStatus(subId, !metered);
} else {
// Right now, we don't have a way to change meteredness of networks other
// than Wi-Fi or Cellular, so just throw an exception.
throw new IllegalStateException("Can't change meteredness of current active network");
}
}
private static String getWifiSsid() {
final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
try {
uiAutomation.adoptShellPermissionIdentity();
final String ssid = getWifiManager().getConnectionInfo().getSSID();
assertNotEquals(WifiManager.UNKNOWN_SSID, ssid);
return ssid;
} finally {
uiAutomation.dropShellPermissionIdentity();
}
}
private static NetworkCapabilities getActiveNetworkCapabilities() {
final Network activeNetwork = getConnectivityManager().getActiveNetwork();
assertNotNull("No active network available", activeNetwork);
return getConnectivityManager().getNetworkCapabilities(activeNetwork);
}
private static void setWifiMeteredStatus(String ssid, boolean metered) throws Exception {
final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
try {
uiAutomation.adoptShellPermissionIdentity();
final WifiConfiguration currentConfig = getWifiConfiguration(ssid);
currentConfig.meteredOverride = metered
? METERED_OVERRIDE_METERED : METERED_OVERRIDE_NONE;
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
getWifiManager().save(currentConfig, createActionListener(
blockingQueue, Integer.MAX_VALUE));
Integer resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS,
TimeUnit.MILLISECONDS);
if (resultCode == null) {
fail("Timed out waiting for meteredness to change; ssid=" + ssid
+ ", metered=" + metered);
} else if (resultCode != Integer.MAX_VALUE) {
fail("Error overriding the meteredness; ssid=" + ssid
+ ", metered=" + metered + ", error=" + resultCode);
}
final boolean success = assertActiveNetworkMetered(metered, false /* throwOnFailure */);
if (!success) {
Log.i(TAG, "Retry connecting to wifi; ssid=" + ssid);
blockingQueue = new LinkedBlockingQueue<>();
getWifiManager().connect(currentConfig, createActionListener(
blockingQueue, Integer.MAX_VALUE));
resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS,
TimeUnit.MILLISECONDS);
if (resultCode == null) {
fail("Timed out waiting for wifi to connect; ssid=" + ssid);
} else if (resultCode != Integer.MAX_VALUE) {
fail("Error connecting to wifi; ssid=" + ssid
+ ", error=" + resultCode);
}
assertActiveNetworkMetered(metered, true /* throwOnFailure */);
}
} finally {
uiAutomation.dropShellPermissionIdentity();
}
}
private static WifiConfiguration getWifiConfiguration(String ssid) {
final List<String> ssids = new ArrayList<>();
for (WifiConfiguration config : getWifiManager().getConfiguredNetworks()) {
if (config.SSID.equals(ssid)) {
return config;
}
ssids.add(config.SSID);
}
fail("Couldn't find the wifi config; ssid=" + ssid
+ ", all=" + Arrays.toString(ssids.toArray()));
return null;
}
private static ActionListener createActionListener(BlockingQueue<Integer> blockingQueue,
int successCode) {
return new ActionListener() {
@Override
public void onSuccess() {
blockingQueue.offer(successCode);
}
@Override
public void onFailure(int reason) {
blockingQueue.offer(reason);
}
};
}
private static void setCellularMeteredStatus(int subId, boolean metered) throws Exception {
final PersistableBundle bundle = new PersistableBundle();
bundle.putStringArray(KEY_CARRIER_METERED_APN_TYPES_STRINGS,
new String[] {ApnSetting.TYPE_MMS_STRING});
ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(getCarrierConfigManager(),
(cm) -> cm.overrideConfig(subId, metered ? null : bundle));
assertActiveNetworkMetered(metered, true /* throwOnFailure */);
}
private static boolean assertActiveNetworkMetered(boolean expectedMeteredStatus,
boolean throwOnFailure) throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
final NetworkCallback networkCallback = new NetworkCallback() {
@Override
public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED);
if (metered == expectedMeteredStatus) {
latch.countDown();
}
}
};
// Registering a callback here guarantees onCapabilitiesChanged is called immediately
// with the current setting. Therefore, if the setting has already been changed,
// this method will return right away, and if not it will wait for the setting to change.
getConnectivityManager().registerDefaultNetworkCallback(networkCallback);
try {
if (!latch.await(TIMEOUT_CHANGE_METEREDNESS_MS, TimeUnit.MILLISECONDS)) {
final String errorMsg = "Timed out waiting for active network metered status "
+ "to change to " + expectedMeteredStatus + "; network = "
+ getConnectivityManager().getActiveNetwork();
if (throwOnFailure) {
fail(errorMsg);
}
Log.w(TAG, errorMsg);
return false;
}
return true;
} finally {
getConnectivityManager().unregisterNetworkCallback(networkCallback);
}
}
public static void setRestrictBackground(boolean enabled) {
if (!isDataSaverSupported()) {
return;
}
setRestrictBackgroundInternal(enabled);
}
private static void setRestrictBackgroundInternal(boolean enabled) {
executeShellCommand("cmd netpolicy set restrict-background " + enabled);
final String output = executeShellCommand("cmd netpolicy get restrict-background");
final String expectedSuffix = enabled ? "enabled" : "disabled";
assertTrue("output '" + output + "' should end with '" + expectedSuffix + "'",
output.endsWith(expectedSuffix));
}
public static boolean isMyRestrictBackgroundStatus(int expectedStatus) {
final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus();
if (expectedStatus != actualStatus) {
Log.d(TAG, "MyRestrictBackgroundStatus: "
+ "Expected: " + restrictBackgroundValueToString(expectedStatus)
+ "; Actual: " + restrictBackgroundValueToString(actualStatus));
return false;
}
return true;
}
// Copied from cts/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
private static 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;
}
}
public static String restrictBackgroundValueToString(int status) {
switch (status) {
case RESTRICT_BACKGROUND_STATUS_DISABLED:
return "DISABLED";
case RESTRICT_BACKGROUND_STATUS_WHITELISTED:
return "WHITELISTED";
case RESTRICT_BACKGROUND_STATUS_ENABLED:
return "ENABLED";
default:
return "UNKNOWN_STATUS_" + status;
}
}
public static String executeShellCommand(String command) {
final String result = runShellCommand(command).trim();
Log.d(TAG, "Output of '" + command + "': '" + result + "'");
return result;
}
public static void assertMyRestrictBackgroundStatus(int expectedStatus) {
final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus();
assertEquals(restrictBackgroundValueToString(expectedStatus),
restrictBackgroundValueToString(actualStatus));
}
public static ConnectivityManager getConnectivityManager() {
if (mCm == null) {
mCm = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
}
return mCm;
}
public static WifiManager getWifiManager() {
if (mWm == null) {
mWm = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
}
return mWm;
}
public static CarrierConfigManager getCarrierConfigManager() {
if (mCarrierConfigManager == null) {
mCarrierConfigManager = (CarrierConfigManager) getContext().getSystemService(
Context.CARRIER_CONFIG_SERVICE);
}
return mCarrierConfigManager;
}
public static Context getContext() {
return getInstrumentation().getContext();
}
public static Instrumentation getInstrumentation() {
return InstrumentationRegistry.getInstrumentation();
}
}