blob: f02ceaa4966bc550138561833b885586bd44a0b7 [file] [log] [blame]
/*
* Copyright (C) 2014 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 android.annotation.TargetApi;
import android.app.job.JobInfo;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
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.os.UserHandle;
import android.platform.test.annotations.RequiresDevice;
import android.util.Log;
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;
/**
* Schedules jobs with the {@link android.app.job.JobScheduler} that have network connectivity
* constraints.
* Requires manipulating the {@link android.net.wifi.WifiManager} to ensure an unmetered network.
* Similarly, requires that the phone be connected to a wifi hotspot, or else the test will fail.
*/
@TargetApi(21)
@RequiresDevice // Emulators don't always have access to wifi/network
public class ConnectivityConstraintTest extends BaseJobSchedulerTest {
private static final String TAG = "ConnectivityConstraintTest";
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";
/** Unique identifier for the job scheduled by this suite of tests. */
public static final int CONNECTIVITY_JOB_ID = ConnectivityConstraintTest.class.hashCode();
private WifiManager mWifiManager;
private ConnectivityManager mCm;
/** Whether the device running these tests supports WiFi. */
private boolean mHasWifi;
/** Whether the device running these tests supports telephony. */
private boolean mHasTelephony;
/** Track whether WiFi was enabled in case we turn it off. */
private boolean mInitialWiFiState;
/** Track whether restrict background policy was enabled in case we turn it off. */
private boolean mInitialRestrictBackground;
private JobInfo.Builder mBuilder;
private TestAppInterface mTestAppInterface;
@Override
public void setUp() throws Exception {
super.setUp();
mWifiManager = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
mCm = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
PackageManager packageManager = mContext.getPackageManager();
mHasWifi = packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI);
mHasTelephony = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
mBuilder = new JobInfo.Builder(CONNECTIVITY_JOB_ID, kJobServiceComponent);
if (mHasWifi) {
mInitialWiFiState = mWifiManager.isWifiEnabled();
ensureSavedWifiNetwork(mWifiManager);
}
mInitialRestrictBackground = SystemUtil
.runShellCommand(getInstrumentation(), RESTRICT_BACKGROUND_GET_CMD)
.contains("enabled");
}
@Override
public void tearDown() throws Exception {
if (mTestAppInterface != null) {
mTestAppInterface.cleanup();
}
mJobScheduler.cancel(CONNECTIVITY_JOB_ID);
// Restore initial restrict background data usage policy
setDataSaverEnabled(mInitialRestrictBackground);
// Ensure that we leave WiFi in its previous state.
if (mHasWifi && mWifiManager.isWifiEnabled() != mInitialWiFiState) {
try {
setWifiState(mInitialWiFiState, mCm, mWifiManager);
} 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);
}
}
super.tearDown();
}
// --------------------------------------------------------------------------------------------
// Positives - schedule jobs under conditions that require them to pass.
// --------------------------------------------------------------------------------------------
/**
* Schedule a job that requires a WiFi connection, and assert that it executes when the device
* is connected to WiFi. This will fail if a wifi connection is unavailable.
*/
public void testUnmeteredConstraintExecutes_withWifi() throws Exception {
if (!mHasWifi) {
Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
return;
}
connectToWifi();
kTestEnvironment.setExpectedExecutions(1);
mJobScheduler.schedule(
mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
.build());
runJob();
assertTrue("Job with unmetered constraint did not fire on WiFi.",
kTestEnvironment.awaitExecution());
}
/**
* Schedule a job with a connectivity constraint, and ensure that it executes on WiFi.
*/
public void testConnectivityConstraintExecutes_withWifi() throws Exception {
if (!mHasWifi) {
Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
return;
}
connectToWifi();
kTestEnvironment.setExpectedExecutions(1);
mJobScheduler.schedule(
mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build());
runJob();
assertTrue("Job with connectivity constraint did not fire on WiFi.",
kTestEnvironment.awaitExecution());
}
/**
* Schedule a job with a generic connectivity constraint, and ensure that it executes on WiFi,
* even with Data Saver on.
*/
public void testConnectivityConstraintExecutes_withWifi_DataSaverOn() throws Exception {
if (!mHasWifi) {
Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
return;
}
connectToWifi();
setDataSaverEnabled(true);
kTestEnvironment.setExpectedExecutions(1);
mJobScheduler.schedule(
mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build());
runJob();
assertTrue("Job with connectivity constraint did not fire on WiFi.",
kTestEnvironment.awaitExecution());
}
/**
* Schedule a job with a generic connectivity constraint, and ensure that it executes
* on a cellular data connection.
*/
public void testConnectivityConstraintExecutes_withMobile() throws Exception {
if (!checkDeviceSupportsMobileData()) {
return;
}
disconnectWifiToConnectToMobile();
kTestEnvironment.setExpectedExecutions(1);
mJobScheduler.schedule(
mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build());
runJob();
assertTrue("Job with connectivity constraint did not fire on mobile.",
kTestEnvironment.awaitExecution());
}
/**
* Schedule a job with a generic connectivity constraint, and ensure that it isn't stopped when
* the device transitions to WiFi.
*/
public void testConnectivityConstraintExecutes_transitionNetworks() throws Exception {
if (!mHasWifi) {
Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
return;
}
if (!checkDeviceSupportsMobileData()) {
return;
}
setDataSaverEnabled(false);
disconnectWifiToConnectToMobile();
kTestEnvironment.setExpectedExecutions(1);
kTestEnvironment.setExpectedStopped();
mJobScheduler.schedule(
mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build());
runJob();
assertTrue("Job with connectivity constraint did not fire on mobile.",
kTestEnvironment.awaitExecution());
connectToWifi();
assertFalse(
"Job with connectivity constraint was stopped when network transitioned to WiFi.",
kTestEnvironment.awaitStopped());
}
/**
* Schedule a job with a metered connectivity constraint, and ensure that it executes
* on a mobile data connection.
*/
public void testConnectivityConstraintExecutes_metered() throws Exception {
if (!checkDeviceSupportsMobileData()) {
return;
}
setDataSaverEnabled(false);
disconnectWifiToConnectToMobile();
kTestEnvironment.setExpectedExecutions(1);
mJobScheduler.schedule(
mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED)
.build());
runJob();
assertTrue("Job with metered connectivity constraint did not fire on mobile.",
kTestEnvironment.awaitExecution());
}
/**
* Schedule a job with a cellular connectivity constraint, and ensure that it executes
* on a mobile data connection and is not stopped when Data Saver is turned on because the app
* is in the foreground.
*/
public void testCellularConstraintExecutedAndStopped_Foreground() throws Exception {
if (!checkDeviceSupportsMobileData()) {
return;
}
setDataSaverEnabled(false);
disconnectWifiToConnectToMobile();
mTestAppInterface = new TestAppInterface(mContext, CONNECTIVITY_JOB_ID);
mTestAppInterface.startAndKeepTestActivity();
mTestAppInterface.scheduleJob(false, true);
runJob();
assertTrue("Job with metered connectivity constraint did not fire on mobile.",
mTestAppInterface.awaitJobStart(30_000));
setDataSaverEnabled(true);
assertFalse(
"Job with metered connectivity constraint for foreground app was stopped when"
+ " Data Saver was turned on.",
mTestAppInterface.awaitJobStop(30_000));
}
// --------------------------------------------------------------------------------------------
// Positives & Negatives - schedule jobs under conditions that require that pass initially and
// then fail with a constraint change.
// --------------------------------------------------------------------------------------------
/**
* Schedule a job with a cellular connectivity constraint, and ensure that it executes
* on a mobile data connection and is stopped when Data Saver is turned on.
*/
public void testCellularConstraintExecutedAndStopped() throws Exception {
if (!checkDeviceSupportsMobileData()) {
return;
}
setDataSaverEnabled(false);
disconnectWifiToConnectToMobile();
kTestEnvironment.setExpectedExecutions(1);
kTestEnvironment.setContinueAfterStart();
kTestEnvironment.setExpectedStopped();
mJobScheduler.schedule(
mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_CELLULAR)
.build());
runJob();
assertTrue("Job with metered connectivity constraint did not fire on mobile.",
kTestEnvironment.awaitExecution());
setDataSaverEnabled(true);
assertTrue(
"Job with metered connectivity constraint was not stopped when Data Saver was "
+ "turned on.",
kTestEnvironment.awaitStopped());
}
// --------------------------------------------------------------------------------------------
// Negatives - schedule jobs under conditions that require that they fail.
// --------------------------------------------------------------------------------------------
/**
* Schedule a job that requires a WiFi connection, and assert that it fails when the device is
* connected to a cellular provider.
* This test assumes that if the device supports a mobile data connection, then this connection
* will be available.
*/
public void testUnmeteredConstraintFails_withMobile() throws Exception {
if (!checkDeviceSupportsMobileData()) {
return;
}
disconnectWifiToConnectToMobile();
kTestEnvironment.setExpectedExecutions(0);
mJobScheduler.schedule(
mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
.build());
runJob();
assertTrue("Job requiring unmetered connectivity still executed on mobile.",
kTestEnvironment.awaitTimeout());
}
/**
* Schedule a job that requires a metered connection, and verify that it does not run when
* the device is not connected to WiFi and Data Saver is on.
*/
public void testMeteredConstraintFails_withMobile_DataSaverOn() throws Exception {
if (!checkDeviceSupportsMobileData()) {
Log.d(TAG, "Skipping test that requires the device be mobile data enabled.");
return;
}
disconnectWifiToConnectToMobile();
setDataSaverEnabled(true);
kTestEnvironment.setExpectedExecutions(0);
mJobScheduler.schedule(
mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_CELLULAR)
.build());
runJob();
assertTrue("Job requiring metered connectivity still executed on WiFi.",
kTestEnvironment.awaitTimeout());
}
/**
* Schedule a job that requires a metered connection, and verify that it does not run when
* the device is connected to an unmetered WiFi provider.
* This test assumes that if the device supports a mobile data connection, then this connection
* will be available.
*/
public void testMeteredConstraintFails_withWiFi() throws Exception {
if (!mHasWifi) {
Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
return;
}
if (!checkDeviceSupportsMobileData()) {
Log.d(TAG, "Skipping test that requires the device be mobile data enabled.");
return;
}
connectToWifi();
kTestEnvironment.setExpectedExecutions(0);
mJobScheduler.schedule(
mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED)
.build());
runJob();
assertTrue("Job requiring metered connectivity still executed on WiFi.",
kTestEnvironment.awaitTimeout());
}
/**
* Schedule a job that requires a cellular connection, and verify that it does not run when
* the device is connected to a WiFi provider.
*/
public void testCellularConstraintFails_withWiFi() throws Exception {
if (!mHasWifi) {
Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
return;
}
if (!checkDeviceSupportsMobileData()) {
Log.d(TAG, "Skipping test that requires the device be mobile data enabled.");
return;
}
connectToWifi();
kTestEnvironment.setExpectedExecutions(0);
mJobScheduler.schedule(
mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_CELLULAR).build());
runJob();
assertTrue("Job requiring cellular connectivity still executed on WiFi.",
kTestEnvironment.awaitTimeout());
}
// --------------------------------------------------------------------------------------------
// Utility methods
// --------------------------------------------------------------------------------------------
/** Asks (not forces) JobScheduler to run the job if functional constraints are met. */
private void runJob() throws Exception {
// Since connectivity is a functional constraint, calling the "run" command without force
// will only get the job to run if the constraint is satisfied.
SystemUtil.runShellCommand(getInstrumentation(), "cmd jobscheduler run"
+ " -u " + UserHandle.myUserId()
+ " " + kJobServiceComponent.getPackageName() + " " + CONNECTIVITY_JOB_ID);
}
/**
* Determine whether the device running these CTS tests should be subject to tests involving
* mobile data.
* @return True if this device will support a mobile data connection.
*/
private boolean checkDeviceSupportsMobileData() {
if (!mHasTelephony) {
Log.d(TAG, "Skipping test that requires telephony features, not supported by this" +
" device");
return false;
}
Network[] networks = mCm.getAllNetworks();
for (Network network : networks) {
if (mCm.getNetworkCapabilities(network)
.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
return true;
}
}
Log.d(TAG, "Skipping test that requires ConnectivityManager.TYPE_MOBILE");
return false;
}
/**
* Ensure WiFi is enabled, and block until we've verified that we are in fact connected.
*/
private void connectToWifi()
throws InterruptedException {
setWifiState(true, mCm, mWifiManager);
}
/**
* Ensure WiFi is disabled, and block until we've verified that we are in fact disconnected.
*/
private void disconnectFromWifi()
throws InterruptedException {
setWifiState(false, mCm, mWifiManager);
}
/** Ensures that the device has a wifi network saved. */
static void ensureSavedWifiNetwork(WifiManager wifiManager) {
final List<WifiConfiguration> savedNetworks =
ShellIdentityUtils.invokeMethodWithShellPermissions(
wifiManager, WifiManager::getConfiguredNetworks);
assertFalse("Need at least one saved wifi network", savedNetworks.isEmpty());
}
/**
* 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}.
*/
static void setWifiState(final boolean enable,
final ConnectivityManager cm, final WifiManager wm) throws InterruptedException {
if (enable != isWiFiConnected(cm, wm)) {
NetworkRequest nr = new NetworkRequest.Builder().addCapability(
NetworkCapabilities.NET_CAPABILITY_NOT_METERED).build();
NetworkTracker tracker = new NetworkTracker(false, enable, cm);
cm.registerNetworkCallback(nr, tracker);
if (enable) {
SystemUtil.runShellCommand("svc wifi enable");
} else {
SystemUtil.runShellCommand("svc wifi disable");
}
tracker.waitForStateChange();
assertTrue("Wifi must be " + (enable ? "connected to" : "disconnected from")
+ " an access point for this test.",
enable == isWiFiConnected(cm, wm));
cm.unregisterNetworkCallback(tracker);
}
}
private static boolean isWiFiConnected(final ConnectivityManager cm, final WifiManager wm) {
return wm.isWifiEnabled() && cm.getActiveNetwork() != null && !cm.isActiveNetworkMetered();
}
/**
* Disconnect from WiFi in an attempt to connect to cellular data. Worth noting that this is
* best effort - there are no public APIs to force connecting to cell data. We disable WiFi
* and wait for a broadcast that we're connected to cell.
* We will not call into this function if the device doesn't support telephony.
* @see #mHasTelephony
* @see #checkDeviceSupportsMobileData()
*/
private void disconnectWifiToConnectToMobile() throws InterruptedException {
if (mHasWifi && mWifiManager.isWifiEnabled()) {
NetworkRequest nr = new NetworkRequest.Builder().build();
NetworkTracker tracker = new NetworkTracker(true, true, mCm);
mCm.registerNetworkCallback(nr, tracker);
disconnectFromWifi();
assertTrue("Device must have access to a metered network for this test.",
tracker.waitForStateChange());
mCm.unregisterNetworkCallback(tracker);
}
}
/**
* Ensures that restrict background data usage policy is turned off.
* If the policy is on, it interferes with tests that relies on metered connection.
*/
private void setDataSaverEnabled(boolean enabled) throws Exception {
SystemUtil.runShellCommand(getInstrumentation(),
enabled ? RESTRICT_BACKGROUND_ON_CMD : RESTRICT_BACKGROUND_OFF_CMD);
}
private static class NetworkTracker extends ConnectivityManager.NetworkCallback {
private final ConnectivityManager mCm;
private final CountDownLatch mReceiveLatch = new CountDownLatch(1);
private final boolean mNetworkMetered;
private final boolean mExpectedConnected;
private final THandler mHandler = new THandler(Looper.getMainLooper());
private class THandler extends Handler {
private static final int MSG_CHECK_ACTIVE_NETWORK = 1;
THandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
checkActiveNetwork();
}
void checkActiveNetwork() {
if (mExpectedConnected) {
if (mNetworkMetered == mCm.isActiveNetworkMetered()) {
mReceiveLatch.countDown();
} else {
postCheckActiveNetwork();
}
} else if (mNetworkMetered == mCm.isActiveNetworkMetered()) {
postCheckActiveNetwork();
} else {
mReceiveLatch.countDown();
}
}
void postCheckActiveNetwork() {
if (mReceiveLatch.getCount() > 0) {
sendEmptyMessageDelayed(MSG_CHECK_ACTIVE_NETWORK, 5000);
}
}
}
private NetworkTracker(boolean networkMetered, boolean expectedConnected,
ConnectivityManager cm) {
mNetworkMetered = networkMetered;
mExpectedConnected = expectedConnected;
mCm = cm;
}
@Override
public void onAvailable(Network network, NetworkCapabilities networkCapabilities,
LinkProperties linkProperties, boolean blocked) {
// Available doesn't mean it's the active network. We need to check that separately.
mHandler.checkActiveNetwork();
}
@Override
public void onLost(Network network) {
mHandler.checkActiveNetwork();
}
boolean waitForStateChange() throws InterruptedException {
return mReceiveLatch.await(30, TimeUnit.SECONDS);
}
}
}