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 java.util.concurrent.CountDownLatch;
32 import java.util.concurrent.TimeUnit;
33 
34 /**
35  * Schedules jobs with the {@link android.app.job.JobScheduler} that have network connectivity
36  * constraints.
37  * Requires manipulating the {@link android.net.wifi.WifiManager} to ensure an unmetered network.
38  * Similarly, requires that the phone be connected to a wifi hotspot, or else the test will fail.
39  */
40 @TargetApi(21)
41 public class ConnectivityConstraintTest extends ConstraintTest {
42     private static final String TAG = "ConnectivityConstraintTest";
43 
44     /** Unique identifier for the job scheduled by this suite of tests. */
45     public static final int CONNECTIVITY_JOB_ID = ConnectivityConstraintTest.class.hashCode();
46 
47     private WifiManager mWifiManager;
48     private ConnectivityManager mCm;
49 
50     /** Whether the device running these tests supports WiFi. */
51     private boolean mHasWifi;
52     /** Whether the device running these tests supports telephony. */
53     private boolean mHasTelephony;
54     /** Track whether WiFi was enabled in case we turn it off. */
55     private boolean mInitialWiFiState;
56 
57     private JobInfo.Builder mBuilder;
58 
59     @Override
setUp()60     public void setUp() throws Exception {
61         super.setUp();
62 
63         mWifiManager = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
64         mCm =
65                 (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
66 
67         PackageManager packageManager = mContext.getPackageManager();
68         mHasWifi = packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI);
69         mHasTelephony = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
70         mBuilder =
71                 new JobInfo.Builder(CONNECTIVITY_JOB_ID, kJobServiceComponent);
72 
73         mInitialWiFiState = mWifiManager.isWifiEnabled();
74     }
75 
76     @Override
tearDown()77     public void tearDown() throws Exception {
78         mJobScheduler.cancel(CONNECTIVITY_JOB_ID);
79 
80         // Ensure that we leave WiFi in its previous state.
81         if (mWifiManager.isWifiEnabled() == mInitialWiFiState) {
82             return;
83         }
84         NetworkInfo.State expectedState = mInitialWiFiState ?
85                 NetworkInfo.State.CONNECTED : NetworkInfo.State.DISCONNECTED;
86         ConnectivityActionReceiver receiver =
87                 new ConnectivityActionReceiver(ConnectivityManager.TYPE_WIFI,
88                         expectedState);
89         IntentFilter filter = new IntentFilter();
90         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
91         mContext.registerReceiver(receiver, filter);
92 
93         assertTrue(mWifiManager.setWifiEnabled(mInitialWiFiState));
94         receiver.waitForStateChange();
95         assertTrue("Failure to restore previous WiFi state",
96                 mWifiManager.isWifiEnabled() == mInitialWiFiState);
97 
98         mContext.unregisterReceiver(receiver);
99     }
100 
101     // --------------------------------------------------------------------------------------------
102     // Positives - schedule jobs under conditions that require them to pass.
103     // --------------------------------------------------------------------------------------------
104 
105     /**
106      * Schedule a job that requires a WiFi connection, and assert that it executes when the device
107      * is connected to WiFi. This will fail if a wifi connection is unavailable.
108      */
testUnmeteredConstraintExecutes_withWifi()109     public void testUnmeteredConstraintExecutes_withWifi() throws Exception {
110         if (!mHasWifi) {
111             Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
112             return;
113         }
114         connectToWiFi();
115 
116         kTestEnvironment.setExpectedExecutions(1);
117         mJobScheduler.schedule(
118                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
119                         .build());
120 
121         sendExpediteStableChargingBroadcast();
122 
123         assertTrue("Job with unmetered constraint did not fire on WiFi.",
124                 kTestEnvironment.awaitExecution());
125     }
126 
127     /**
128      * Schedule a job with a connectivity constraint, and ensure that it executes on WiFi.
129      */
testConnectivityConstraintExecutes_withWifi()130     public void testConnectivityConstraintExecutes_withWifi() throws Exception {
131         if (!mHasWifi) {
132             Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
133             return;
134         }
135         connectToWiFi();
136 
137         kTestEnvironment.setExpectedExecutions(1);
138         mJobScheduler.schedule(
139                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
140                         .build());
141 
142         sendExpediteStableChargingBroadcast();
143 
144         assertTrue("Job with connectivity constraint did not fire on WiFi.",
145                 kTestEnvironment.awaitExecution());
146     }
147 
148     /**
149      * Schedule a job with a generic connectivity constraint, and ensure that it executes
150      * on a cellular data connection.
151      */
testConnectivityConstraintExecutes_withMobile()152     public void testConnectivityConstraintExecutes_withMobile() throws Exception {
153         if (!checkDeviceSupportsMobileData()) {
154             return;
155         }
156         disconnectWifiToConnectToMobile();
157 
158         kTestEnvironment.setExpectedExecutions(1);
159         mJobScheduler.schedule(
160                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
161                         .build());
162 
163         sendExpediteStableChargingBroadcast();
164 
165         assertTrue("Job with connectivity constraint did not fire on mobile.",
166                 kTestEnvironment.awaitExecution());
167     }
168 
169     /**
170      * Schedule a job with a metered connectivity constraint, and ensure that it executes
171      * on a mobile data connection.
172      */
testConnectivityConstraintExecutes_metered()173     public void testConnectivityConstraintExecutes_metered() throws Exception {
174         if (!checkDeviceSupportsMobileData()) {
175             return;
176         }
177         disconnectWifiToConnectToMobile();
178 
179         kTestEnvironment.setExpectedExecutions(1);
180         mJobScheduler.schedule(
181                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED)
182                         .build());
183 
184         sendExpediteStableChargingBroadcast();
185         assertTrue("Job with metered connectivity constraint did not fire on mobile.",
186                 kTestEnvironment.awaitExecution());
187     }
188 
189     // --------------------------------------------------------------------------------------------
190     // Negatives - schedule jobs under conditions that require that they fail.
191     // --------------------------------------------------------------------------------------------
192 
193     /**
194      * Schedule a job that requires a WiFi connection, and assert that it fails when the device is
195      * connected to a cellular provider.
196      * This test assumes that if the device supports a mobile data connection, then this connection
197      * will be available.
198      */
testUnmeteredConstraintFails_withMobile()199     public void testUnmeteredConstraintFails_withMobile() throws Exception {
200         if (!checkDeviceSupportsMobileData()) {
201             return;
202         }
203         disconnectWifiToConnectToMobile();
204 
205         kTestEnvironment.setExpectedExecutions(0);
206         mJobScheduler.schedule(
207                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
208                         .build());
209         sendExpediteStableChargingBroadcast();
210 
211         assertTrue("Job requiring unmetered connectivity still executed on mobile.",
212                 kTestEnvironment.awaitTimeout());
213     }
214 
215     /**
216      * Schedule a job that requires a metered connection, and verify that it does not run when
217      * the device is connected to a WiFi provider.
218      * This test assumes that if the device supports a mobile data connection, then this connection
219      * will be available.
220      */
testMeteredConstraintFails_withWiFi()221     public void testMeteredConstraintFails_withWiFi() throws Exception {
222         if (!mHasWifi) {
223             Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
224             return;
225         }
226         if (!checkDeviceSupportsMobileData()) {
227             Log.d(TAG, "Skipping test that requires the device be mobile data enabled.");
228             return;
229         }
230         connectToWiFi();
231 
232         kTestEnvironment.setExpectedExecutions(0);
233         mJobScheduler.schedule(
234                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED)
235                         .build());
236         sendExpediteStableChargingBroadcast();
237 
238         assertTrue("Job requiring metered connectivity still executed on WiFi.",
239                 kTestEnvironment.awaitTimeout());
240     }
241 
242     // --------------------------------------------------------------------------------------------
243     // Utility methods
244     // --------------------------------------------------------------------------------------------
245 
246     /**
247      * Determine whether the device running these CTS tests should be subject to tests involving
248      * mobile data.
249      * @return True if this device will support a mobile data connection.
250      */
checkDeviceSupportsMobileData()251     private boolean checkDeviceSupportsMobileData() {
252         if (!mHasTelephony) {
253             Log.d(TAG, "Skipping test that requires telephony features, not supported by this" +
254                     " device");
255             return false;
256         }
257         if (mCm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE) == null) {
258             Log.d(TAG, "Skipping test that requires ConnectivityManager.TYPE_MOBILE");
259             return false;
260         }
261         return true;
262     }
263 
264     /**
265      * Ensure WiFi is enabled, and block until we've verified that we are in fact connected.
266      * Taken from {@link android.net.http.cts.ApacheHttpClientTest}.
267      */
connectToWiFi()268     private void connectToWiFi() throws InterruptedException {
269         if (!mWifiManager.isWifiEnabled()) {
270             ConnectivityActionReceiver receiver =
271                     new ConnectivityActionReceiver(ConnectivityManager.TYPE_WIFI,
272                             NetworkInfo.State.CONNECTED);
273             IntentFilter filter = new IntentFilter();
274             filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
275             mContext.registerReceiver(receiver, filter);
276 
277             assertTrue(mWifiManager.setWifiEnabled(true));
278             assertTrue("Wifi must be configured to connect to an access point for this test.",
279                     receiver.waitForStateChange());
280 
281             mContext.unregisterReceiver(receiver);
282         }
283     }
284 
285     /**
286      * Disconnect from WiFi in an attempt to connect to cellular data. Worth noting that this is
287      * best effort - there are no public APIs to force connecting to cell data. We disable WiFi
288      * and wait for a broadcast that we're connected to cell.
289      * We will not call into this function if the device doesn't support telephony.
290      * @see #mHasTelephony
291      * @see #checkDeviceSupportsMobileData()
292      */
disconnectWifiToConnectToMobile()293     private void disconnectWifiToConnectToMobile() throws InterruptedException {
294         if (mHasWifi && mWifiManager.isWifiEnabled()) {
295             ConnectivityActionReceiver connectMobileReceiver =
296                     new ConnectivityActionReceiver(ConnectivityManager.TYPE_MOBILE,
297                             NetworkInfo.State.CONNECTED);
298             ConnectivityActionReceiver disconnectWifiReceiver =
299                     new ConnectivityActionReceiver(ConnectivityManager.TYPE_WIFI,
300                             NetworkInfo.State.DISCONNECTED);
301             IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
302             mContext.registerReceiver(connectMobileReceiver, filter);
303             mContext.registerReceiver(disconnectWifiReceiver, filter);
304 
305             assertTrue(mWifiManager.setWifiEnabled(false));
306             assertTrue("Failure disconnecting from WiFi.",
307                     disconnectWifiReceiver.waitForStateChange());
308             assertTrue("Device must have access to a metered network for this test.",
309                     connectMobileReceiver.waitForStateChange());
310 
311             mContext.unregisterReceiver(connectMobileReceiver);
312             mContext.unregisterReceiver(disconnectWifiReceiver);
313         }
314     }
315 
316     /** Capture the last connectivity change's network type and state. */
317     private class ConnectivityActionReceiver extends BroadcastReceiver {
318 
319         private final CountDownLatch mReceiveLatch = new CountDownLatch(1);
320 
321         private final int mNetworkType;
322 
323         private final NetworkInfo.State mExpectedState;
324 
ConnectivityActionReceiver(int networkType, NetworkInfo.State expectedState)325         ConnectivityActionReceiver(int networkType, NetworkInfo.State expectedState) {
326             mNetworkType = networkType;
327             mExpectedState = expectedState;
328         }
329 
onReceive(Context context, Intent intent)330         public void onReceive(Context context, Intent intent) {
331             // Dealing with a connectivity changed event for this network type.
332             final int networkTypeChanged =
333                     intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, -1);
334             if (networkTypeChanged == -1) {
335                 Log.e(TAG, "No network type provided in intent");
336                 return;
337             }
338 
339             if (networkTypeChanged != mNetworkType) {
340                 // Only track changes for the connectivity event that we are interested in.
341                 return;
342             }
343             // Pull out the NetworkState object that we're interested in. Necessary because
344             // the ConnectivityManager will filter on uid for background connectivity.
345             NetworkInfo[] allNetworkInfo = mCm.getAllNetworkInfo();
346             NetworkInfo networkInfo = null;
347             for (int i=0; i<allNetworkInfo.length; i++) {
348                 NetworkInfo ni = allNetworkInfo[i];
349                 if (ni.getType() == mNetworkType) {
350                     networkInfo =  ni;
351                     break;
352                 }
353             }
354             if (networkInfo == null) {
355                 Log.e(TAG, "Could not find correct network type.");
356                 return;
357             }
358 
359             NetworkInfo.State networkState = networkInfo.getState();
360             Log.i(TAG, "Network type: " + mNetworkType + " State: " + networkState);
361             if (networkState == mExpectedState) {
362                 mReceiveLatch.countDown();
363             }
364         }
365 
waitForStateChange()366         public boolean waitForStateChange() throws InterruptedException {
367             return mReceiveLatch.await(30, TimeUnit.SECONDS) || hasExpectedState();
368         }
369 
hasExpectedState()370         private boolean hasExpectedState() {
371             return mExpectedState == mCm.getNetworkInfo(mNetworkType).getState();
372         }
373     }
374 
375 }
376