1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.jobscheduler.cts;
17 
18 
19 import android.annotation.TargetApi;
20 import android.app.job.JobInfo;
21 import android.content.BroadcastReceiver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.pm.PackageManager;
26 import android.net.ConnectivityManager;
27 import android.net.NetworkInfo;
28 import android.net.wifi.WifiManager;
29 import android.util.Log;
30 
31 import com.android.compatibility.common.util.SystemUtil;
32 
33 import java.util.concurrent.CountDownLatch;
34 import java.util.concurrent.TimeUnit;
35 
36 /**
37  * Schedules jobs with the {@link android.app.job.JobScheduler} that have network connectivity
38  * constraints.
39  * Requires manipulating the {@link android.net.wifi.WifiManager} to ensure an unmetered network.
40  * Similarly, requires that the phone be connected to a wifi hotspot, or else the test will fail.
41  */
42 @TargetApi(21)
43 public class ConnectivityConstraintTest extends ConstraintTest {
44     private static final String TAG = "ConnectivityConstraintTest";
45     private static final String RESTRICT_BACKGROUND_GET_CMD =
46             "cmd netpolicy get restrict-background";
47     private static final String RESTRICT_BACKGROUND_ON_CMD =
48             "cmd netpolicy set restrict-background true";
49     private static final String RESTRICT_BACKGROUND_OFF_CMD =
50             "cmd netpolicy set restrict-background false";
51 
52     /** Unique identifier for the job scheduled by this suite of tests. */
53     public static final int CONNECTIVITY_JOB_ID = ConnectivityConstraintTest.class.hashCode();
54 
55     private WifiManager mWifiManager;
56     private ConnectivityManager mCm;
57 
58     /** Whether the device running these tests supports WiFi. */
59     private boolean mHasWifi;
60     /** Whether the device running these tests supports telephony. */
61     private boolean mHasTelephony;
62     /** Track whether WiFi was enabled in case we turn it off. */
63     private boolean mInitialWiFiState;
64     /** Track whether restrict background policy was enabled in case we turn it off. */
65     private boolean mInitialRestrictBackground;
66 
67     private JobInfo.Builder mBuilder;
68 
69     private TestAppInterface mTestAppInterface;
70 
71     @Override
setUp()72     public void setUp() throws Exception {
73         super.setUp();
74 
75         mWifiManager = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
76         mCm =
77                 (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
78 
79         PackageManager packageManager = mContext.getPackageManager();
80         mHasWifi = packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI);
81         mHasTelephony = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
82         mBuilder =
83                 new JobInfo.Builder(CONNECTIVITY_JOB_ID, kJobServiceComponent);
84 
85         mInitialWiFiState = mWifiManager.isWifiEnabled();
86         mInitialRestrictBackground = SystemUtil
87                 .runShellCommand(getInstrumentation(), RESTRICT_BACKGROUND_GET_CMD)
88                 .contains("enabled");
89     }
90 
91     @Override
tearDown()92     public void tearDown() throws Exception {
93         if (mTestAppInterface != null) {
94             mTestAppInterface.cleanup();
95         }
96         mJobScheduler.cancel(CONNECTIVITY_JOB_ID);
97 
98         // Restore initial restrict background data usage policy
99         setDataSaverEnabled(mInitialRestrictBackground);
100 
101         // Ensure that we leave WiFi in its previous state.
102         if (mWifiManager.isWifiEnabled() != mInitialWiFiState) {
103             setWifiState(mInitialWiFiState, mContext, mCm, mWifiManager);
104         }
105 
106         super.tearDown();
107     }
108 
109     // --------------------------------------------------------------------------------------------
110     // Positives - schedule jobs under conditions that require them to pass.
111     // --------------------------------------------------------------------------------------------
112 
113     /**
114      * Schedule a job that requires a WiFi connection, and assert that it executes when the device
115      * is connected to WiFi. This will fail if a wifi connection is unavailable.
116      */
testUnmeteredConstraintExecutes_withWifi()117     public void testUnmeteredConstraintExecutes_withWifi() throws Exception {
118         if (!mHasWifi) {
119             Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
120             return;
121         }
122         connectToWifi();
123 
124         kTestEnvironment.setExpectedExecutions(1);
125         mJobScheduler.schedule(
126                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
127                         .build());
128 
129         sendExpediteStableChargingBroadcast();
130 
131         assertTrue("Job with unmetered constraint did not fire on WiFi.",
132                 kTestEnvironment.awaitExecution());
133     }
134 
135     /**
136      * Schedule a job with a connectivity constraint, and ensure that it executes on WiFi.
137      */
testConnectivityConstraintExecutes_withWifi()138     public void testConnectivityConstraintExecutes_withWifi() throws Exception {
139         if (!mHasWifi) {
140             Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
141             return;
142         }
143         connectToWifi();
144 
145         kTestEnvironment.setExpectedExecutions(1);
146         mJobScheduler.schedule(
147                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
148                         .build());
149 
150         sendExpediteStableChargingBroadcast();
151 
152         assertTrue("Job with connectivity constraint did not fire on WiFi.",
153                 kTestEnvironment.awaitExecution());
154     }
155 
156     /**
157      * Schedule a job with a generic connectivity constraint, and ensure that it executes on WiFi,
158      * even with Data Saver on.
159      */
testConnectivityConstraintExecutes_withWifi_DataSaverOn()160     public void testConnectivityConstraintExecutes_withWifi_DataSaverOn() throws Exception {
161         if (!mHasWifi) {
162             Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
163             return;
164         }
165         connectToWifi();
166         setDataSaverEnabled(true);
167 
168         kTestEnvironment.setExpectedExecutions(1);
169         mJobScheduler.schedule(
170                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
171                         .build());
172 
173         sendExpediteStableChargingBroadcast();
174 
175         assertTrue("Job with connectivity constraint did not fire on WiFi.",
176                 kTestEnvironment.awaitExecution());
177     }
178 
179     /**
180      * Schedule a job with a generic connectivity constraint, and ensure that it executes
181      * on a cellular data connection.
182      */
testConnectivityConstraintExecutes_withMobile()183     public void testConnectivityConstraintExecutes_withMobile() throws Exception {
184         if (!checkDeviceSupportsMobileData()) {
185             return;
186         }
187         disconnectWifiToConnectToMobile();
188 
189         kTestEnvironment.setExpectedExecutions(1);
190         mJobScheduler.schedule(
191                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
192                         .build());
193 
194         sendExpediteStableChargingBroadcast();
195 
196         assertTrue("Job with connectivity constraint did not fire on mobile.",
197                 kTestEnvironment.awaitExecution());
198     }
199 
200     /**
201      * Schedule a job with a generic connectivity constraint, and ensure that it isn't stopped when
202      * the device transitions to WiFi.
203      */
testConnectivityConstraintExecutes_transitionNetworks()204     public void testConnectivityConstraintExecutes_transitionNetworks() throws Exception {
205         if (!mHasWifi) {
206             Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
207             return;
208         }
209         if (!checkDeviceSupportsMobileData()) {
210             return;
211         }
212         setDataSaverEnabled(false);
213         disconnectWifiToConnectToMobile();
214 
215         kTestEnvironment.setExpectedExecutions(1);
216         kTestEnvironment.setExpectedStopped();
217         mJobScheduler.schedule(
218                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
219                         .build());
220 
221         sendExpediteStableChargingBroadcast();
222 
223         assertTrue("Job with connectivity constraint did not fire on mobile.",
224                 kTestEnvironment.awaitExecution());
225 
226         connectToWifi();
227         assertFalse(
228                 "Job with connectivity constraint was stopped when network transitioned to WiFi.",
229                 kTestEnvironment.awaitStopped());
230     }
231 
232     /**
233      * Schedule a job with a metered connectivity constraint, and ensure that it executes
234      * on a mobile data connection.
235      */
testConnectivityConstraintExecutes_metered()236     public void testConnectivityConstraintExecutes_metered() throws Exception {
237         if (!checkDeviceSupportsMobileData()) {
238             return;
239         }
240         setDataSaverEnabled(false);
241         disconnectWifiToConnectToMobile();
242 
243         kTestEnvironment.setExpectedExecutions(1);
244         mJobScheduler.schedule(
245                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED)
246                         .build());
247 
248         sendExpediteStableChargingBroadcast();
249         assertTrue("Job with metered connectivity constraint did not fire on mobile.",
250                 kTestEnvironment.awaitExecution());
251     }
252 
253     /**
254      * Schedule a job with a cellular connectivity constraint, and ensure that it executes
255      * on a mobile data connection and is not stopped when Data Saver is turned on because the app
256      * is in the foreground.
257      */
testCellularConstraintExecutedAndStopped_Foreground()258     public void testCellularConstraintExecutedAndStopped_Foreground() throws Exception {
259         if (!checkDeviceSupportsMobileData()) {
260             return;
261         }
262         setDataSaverEnabled(false);
263         disconnectWifiToConnectToMobile();
264         mTestAppInterface = new TestAppInterface(mContext, CONNECTIVITY_JOB_ID);
265         mTestAppInterface.startAndKeepTestActivity();
266 
267         mTestAppInterface.scheduleJob(false, true);
268 
269         sendExpediteStableChargingBroadcast();
270         assertTrue("Job with metered connectivity constraint did not fire on mobile.",
271                 mTestAppInterface.awaitJobStart(30_000));
272 
273         setDataSaverEnabled(true);
274         assertFalse(
275                 "Job with metered connectivity constraint for foreground app was stopped when"
276                         + " Data Saver was turned on.",
277                 mTestAppInterface.awaitJobStop(30_000));
278     }
279 
280     // --------------------------------------------------------------------------------------------
281     // Positives & Negatives - schedule jobs under conditions that require that pass initially and
282     // then fail with a constraint change.
283     // --------------------------------------------------------------------------------------------
284 
285     /**
286      * Schedule a job with a cellular connectivity constraint, and ensure that it executes
287      * on a mobile data connection and is stopped when Data Saver is turned on.
288      */
testCellularConstraintExecutedAndStopped()289     public void testCellularConstraintExecutedAndStopped() throws Exception {
290         if (!checkDeviceSupportsMobileData()) {
291             return;
292         }
293         setDataSaverEnabled(false);
294         disconnectWifiToConnectToMobile();
295 
296         kTestEnvironment.setExpectedExecutions(1);
297         kTestEnvironment.setContinueAfterStart();
298         kTestEnvironment.setExpectedStopped();
299         mJobScheduler.schedule(
300                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_CELLULAR)
301                         .build());
302 
303         sendExpediteStableChargingBroadcast();
304         assertTrue("Job with metered connectivity constraint did not fire on mobile.",
305                 kTestEnvironment.awaitExecution());
306 
307         setDataSaverEnabled(true);
308         assertTrue(
309                 "Job with metered connectivity constraint was not stopped when Data Saver was "
310                         + "turned on.",
311                 kTestEnvironment.awaitStopped());
312     }
313 
314     // --------------------------------------------------------------------------------------------
315     // Negatives - schedule jobs under conditions that require that they fail.
316     // --------------------------------------------------------------------------------------------
317 
318     /**
319      * Schedule a job that requires a WiFi connection, and assert that it fails when the device is
320      * connected to a cellular provider.
321      * This test assumes that if the device supports a mobile data connection, then this connection
322      * will be available.
323      */
testUnmeteredConstraintFails_withMobile()324     public void testUnmeteredConstraintFails_withMobile() throws Exception {
325         if (!checkDeviceSupportsMobileData()) {
326             return;
327         }
328         disconnectWifiToConnectToMobile();
329 
330         kTestEnvironment.setExpectedExecutions(0);
331         mJobScheduler.schedule(
332                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
333                         .build());
334         sendExpediteStableChargingBroadcast();
335 
336         assertTrue("Job requiring unmetered connectivity still executed on mobile.",
337                 kTestEnvironment.awaitTimeout());
338     }
339 
340     /**
341      * Schedule a job that requires a metered connection, and verify that it does not run when
342      * the device is not connected to WiFi and Data Saver is on.
343      */
testMeteredConstraintFails_withMobile_DataSaverOn()344     public void testMeteredConstraintFails_withMobile_DataSaverOn() throws Exception {
345         if (!checkDeviceSupportsMobileData()) {
346             Log.d(TAG, "Skipping test that requires the device be mobile data enabled.");
347             return;
348         }
349         disconnectWifiToConnectToMobile();
350         setDataSaverEnabled(true);
351 
352         kTestEnvironment.setExpectedExecutions(0);
353         mJobScheduler.schedule(
354                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_CELLULAR)
355                         .build());
356         sendExpediteStableChargingBroadcast();
357 
358         assertTrue("Job requiring metered connectivity still executed on WiFi.",
359                 kTestEnvironment.awaitTimeout());
360     }
361 
362     /**
363      * Schedule a job that requires a metered connection, and verify that it does not run when
364      * the device is connected to a WiFi provider.
365      * This test assumes that if the device supports a mobile data connection, then this connection
366      * will be available.
367      */
testMeteredConstraintFails_withWiFi()368     public void testMeteredConstraintFails_withWiFi() throws Exception {
369         if (!mHasWifi) {
370             Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
371             return;
372         }
373         if (!checkDeviceSupportsMobileData()) {
374             Log.d(TAG, "Skipping test that requires the device be mobile data enabled.");
375             return;
376         }
377         connectToWifi();
378 
379         kTestEnvironment.setExpectedExecutions(0);
380         mJobScheduler.schedule(
381                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED)
382                         .build());
383         sendExpediteStableChargingBroadcast();
384 
385         assertTrue("Job requiring metered connectivity still executed on WiFi.",
386                 kTestEnvironment.awaitTimeout());
387     }
388 
389     // --------------------------------------------------------------------------------------------
390     // Utility methods
391     // --------------------------------------------------------------------------------------------
392 
393     /**
394      * Determine whether the device running these CTS tests should be subject to tests involving
395      * mobile data.
396      * @return True if this device will support a mobile data connection.
397      */
checkDeviceSupportsMobileData()398     private boolean checkDeviceSupportsMobileData() {
399         if (!mHasTelephony) {
400             Log.d(TAG, "Skipping test that requires telephony features, not supported by this" +
401                     " device");
402             return false;
403         }
404         if (mCm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE) == null) {
405             Log.d(TAG, "Skipping test that requires ConnectivityManager.TYPE_MOBILE");
406             return false;
407         }
408         return true;
409     }
410 
411     /**
412      * Ensure WiFi is enabled, and block until we've verified that we are in fact connected.
413      */
connectToWifi()414     private void connectToWifi()
415             throws InterruptedException {
416         setWifiState(true, mContext, mCm, mWifiManager);
417     }
418 
419     /**
420      * Ensure WiFi is disabled, and block until we've verified that we are in fact disconnected.
421      */
disconnectFromWifi()422     private void disconnectFromWifi()
423             throws InterruptedException {
424         setWifiState(false, mContext, mCm, mWifiManager);
425     }
426 
427     /**
428      * Set Wifi connection to specific state , and block until we've verified
429      * that we are in the state.
430      * Taken from {@link android.net.http.cts.ApacheHttpClientTest}.
431      */
setWifiState(boolean enable, Context context, ConnectivityManager cm, WifiManager wm)432     static void setWifiState(boolean enable, Context context, ConnectivityManager cm,
433             WifiManager wm) throws InterruptedException  {
434         if (enable != wm.isWifiEnabled()) {
435             NetworkInfo.State expectedState = enable ?
436                     NetworkInfo.State.CONNECTED : NetworkInfo.State.DISCONNECTED;
437             ConnectivityActionReceiver receiver =
438                     new ConnectivityActionReceiver(ConnectivityManager.TYPE_WIFI,
439                             expectedState, cm);
440             IntentFilter filter = new IntentFilter();
441             filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
442             context.registerReceiver(receiver, filter);
443 
444             if (enable) {
445                 SystemUtil.runShellCommand("svc wifi enable");
446             } else {
447                 SystemUtil.runShellCommand("svc wifi disable");
448             }
449             assertTrue("Wifi must be configured to " + (enable ? "connect" : "disconnect")
450                             + " to an access point for this test.",
451                     receiver.waitForStateChange());
452 
453             context.unregisterReceiver(receiver);
454         }
455     }
456 
457     /**
458      * Disconnect from WiFi in an attempt to connect to cellular data. Worth noting that this is
459      * best effort - there are no public APIs to force connecting to cell data. We disable WiFi
460      * and wait for a broadcast that we're connected to cell.
461      * We will not call into this function if the device doesn't support telephony.
462      * @see #mHasTelephony
463      * @see #checkDeviceSupportsMobileData()
464      */
disconnectWifiToConnectToMobile()465     private void disconnectWifiToConnectToMobile() throws InterruptedException {
466         if (mHasWifi && mWifiManager.isWifiEnabled()) {
467             disconnectFromWifi();
468             ConnectivityActionReceiver connectMobileReceiver =
469                     new ConnectivityActionReceiver(ConnectivityManager.TYPE_MOBILE,
470                             NetworkInfo.State.CONNECTED, mCm);
471             IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
472             mContext.registerReceiver(connectMobileReceiver, filter);
473 
474 
475             assertTrue("Device must have access to a metered network for this test.",
476                     connectMobileReceiver.waitForStateChange());
477 
478             mContext.unregisterReceiver(connectMobileReceiver);
479         }
480     }
481 
482     /**
483      * Ensures that restrict background data usage policy is turned off.
484      * If the policy is on, it interferes with tests that relies on metered connection.
485      */
setDataSaverEnabled(boolean enabled)486     private void setDataSaverEnabled(boolean enabled) throws Exception {
487         SystemUtil.runShellCommand(getInstrumentation(),
488                 enabled ? RESTRICT_BACKGROUND_ON_CMD : RESTRICT_BACKGROUND_OFF_CMD);
489     }
490 
491     /** Capture the last connectivity change's network type and state. */
492     private static class ConnectivityActionReceiver extends BroadcastReceiver {
493 
494         private final CountDownLatch mReceiveLatch = new CountDownLatch(1);
495 
496         private final int mNetworkType;
497 
498         private final NetworkInfo.State mExpectedState;
499 
500         private final ConnectivityManager mCm;
501 
ConnectivityActionReceiver(int networkType, NetworkInfo.State expectedState, ConnectivityManager cm)502         ConnectivityActionReceiver(int networkType, NetworkInfo.State expectedState,
503                 ConnectivityManager cm) {
504             mNetworkType = networkType;
505             mExpectedState = expectedState;
506             mCm = cm;
507         }
508 
onReceive(Context context, Intent intent)509         public void onReceive(Context context, Intent intent) {
510             // Dealing with a connectivity changed event for this network type.
511             final int networkTypeChanged =
512                     intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, -1);
513             if (networkTypeChanged == -1) {
514                 Log.e(TAG, "No network type provided in intent");
515                 return;
516             }
517 
518             if (networkTypeChanged != mNetworkType) {
519                 // Only track changes for the connectivity event that we are interested in.
520                 return;
521             }
522             // Pull out the NetworkState object that we're interested in. Necessary because
523             // the ConnectivityManager will filter on uid for background connectivity.
524             NetworkInfo[] allNetworkInfo = mCm.getAllNetworkInfo();
525             NetworkInfo networkInfo = null;
526             for (int i=0; i<allNetworkInfo.length; i++) {
527                 NetworkInfo ni = allNetworkInfo[i];
528                 if (ni.getType() == mNetworkType) {
529                     networkInfo =  ni;
530                     break;
531                 }
532             }
533             if (networkInfo == null) {
534                 Log.e(TAG, "Could not find correct network type.");
535                 return;
536             }
537 
538             NetworkInfo.State networkState = networkInfo.getState();
539             Log.i(TAG, "Network type: " + mNetworkType + " State: " + networkState);
540             if (networkState == mExpectedState) {
541                 mReceiveLatch.countDown();
542             }
543         }
544 
waitForStateChange()545         public boolean waitForStateChange() throws InterruptedException {
546             return mReceiveLatch.await(30, TimeUnit.SECONDS) || hasExpectedState();
547         }
548 
hasExpectedState()549         private boolean hasExpectedState() {
550             return mExpectedState == mCm.getNetworkInfo(mNetworkType).getState();
551         }
552     }
553 
554 }
555