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         // Ensure that we leave WiFi in its previous state.
79         NetworkInfo.State expectedState = mInitialWiFiState ?
80             NetworkInfo.State.CONNECTED : NetworkInfo.State.DISCONNECTED;
81         ConnectivityActionReceiver receiver =
82             new ConnectivityActionReceiver(ConnectivityManager.TYPE_WIFI,
83                                            expectedState);
84         IntentFilter filter = new IntentFilter();
85         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
86         mContext.registerReceiver(receiver, filter);
87 
88         assertTrue(mWifiManager.setWifiEnabled(mInitialWiFiState));
89         assertTrue("Failure to restore previous WiFi state.",
90                     receiver.waitForStateChange());
91 
92         mContext.unregisterReceiver(receiver);
93     }
94 
95     // --------------------------------------------------------------------------------------------
96     // Positives - schedule jobs under conditions that require them to pass.
97     // --------------------------------------------------------------------------------------------
98 
99     /**
100      * Schedule a job that requires a WiFi connection, and assert that it executes when the device
101      * is connected to WiFi. This will fail if a wifi connection is unavailable.
102      */
testUnmeteredConstraintExecutes_withWifi()103     public void testUnmeteredConstraintExecutes_withWifi() throws Exception {
104         if (!mHasWifi) {
105             Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
106             return;
107         }
108         connectToWiFi();
109 
110         kTestEnvironment.setExpectedExecutions(1);
111         mJobScheduler.schedule(
112                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
113                         .build());
114 
115         sendExpediteStableChargingBroadcast();
116 
117         assertTrue("Job with unmetered constraint did not fire on WiFi.",
118                 kTestEnvironment.awaitExecution());
119     }
120 
121     /**
122      * Schedule a job with a connectivity constraint, and ensure that it executes on WiFi.
123      */
testConnectivityConstraintExecutes_withWifi()124     public void testConnectivityConstraintExecutes_withWifi() throws Exception {
125         if (!mHasWifi) {
126             Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
127             return;
128         }
129         connectToWiFi();
130 
131         kTestEnvironment.setExpectedExecutions(1);
132         mJobScheduler.schedule(
133                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
134                         .build());
135 
136         sendExpediteStableChargingBroadcast();
137 
138         assertTrue("Job with connectivity constraint did not fire on WiFi.",
139                 kTestEnvironment.awaitExecution());
140     }
141 
142     /**
143      * Schedule a job with a connectivity constraint, and ensure that it executes on on a mobile
144      * data connection.
145      */
testConnectivityConstraintExecutes_withMobile()146     public void testConnectivityConstraintExecutes_withMobile() throws Exception {
147         if (!checkDeviceSupportsMobileData()) {
148             return;
149         }
150         disconnectWifiToConnectToMobile();
151 
152         kTestEnvironment.setExpectedExecutions(1);
153         mJobScheduler.schedule(
154                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
155                         .build());
156 
157         sendExpediteStableChargingBroadcast();
158 
159         assertTrue("Job with connectivity constraint did not fire on mobile.",
160                 kTestEnvironment.awaitExecution());
161     }
162 
163     // --------------------------------------------------------------------------------------------
164     // Negatives - schedule jobs under conditions that require that they fail.
165     // --------------------------------------------------------------------------------------------
166 
167     /**
168      * Schedule a job that requires a WiFi connection, and assert that it fails when the device is
169      * connected to a cellular provider.
170      * This test assumes that if the device supports a mobile data connection, then this connection
171      * will be available.
172      */
testUnmeteredConstraintFails_withMobile()173     public void testUnmeteredConstraintFails_withMobile() throws Exception {
174         if (!checkDeviceSupportsMobileData()) {
175             return;
176         }
177         disconnectWifiToConnectToMobile();
178 
179         kTestEnvironment.setExpectedExecutions(0);
180         mJobScheduler.schedule(
181                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
182                         .build());
183         sendExpediteStableChargingBroadcast();
184 
185         assertTrue("Job requiring unmetered connectivity still executed on mobile.",
186                 kTestEnvironment.awaitTimeout());
187     }
188 
189     /**
190      * Determine whether the device running these CTS tests should be subject to tests involving
191      * mobile data.
192      * @return True if this device will support a mobile data connection.
193      */
checkDeviceSupportsMobileData()194     private boolean checkDeviceSupportsMobileData() {
195         if (!mHasTelephony) {
196             Log.d(TAG, "Skipping test that requires telephony features, not supported by this" +
197                     " device");
198             return false;
199         }
200         if (mCm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE) == null) {
201             Log.d(TAG, "Skipping test that requires ConnectivityManager.TYPE_MOBILE");
202             return false;
203         }
204         return true;
205     }
206 
207     /**
208      * Ensure WiFi is enabled, and block until we've verified that we are in fact connected.
209      * Taken from {@link android.net.http.cts.ApacheHttpClientTest}.
210      */
connectToWiFi()211     private void connectToWiFi() throws InterruptedException {
212         if (!mWifiManager.isWifiEnabled()) {
213             ConnectivityActionReceiver receiver =
214                     new ConnectivityActionReceiver(ConnectivityManager.TYPE_WIFI,
215                             NetworkInfo.State.CONNECTED);
216             IntentFilter filter = new IntentFilter();
217             filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
218             mContext.registerReceiver(receiver, filter);
219 
220             assertTrue(mWifiManager.setWifiEnabled(true));
221             assertTrue("Wifi must be configured to connect to an access point for this test.",
222                     receiver.waitForStateChange());
223 
224             mContext.unregisterReceiver(receiver);
225         }
226     }
227 
228     /**
229      * Disconnect from WiFi in an attempt to connect to cellular data. Worth noting that this is
230      * best effort - there are no public APIs to force connecting to cell data. We disable WiFi
231      * and wait for a broadcast that we're connected to cell.
232      * We will not call into this function if the device doesn't support telephony.
233      * @see #mHasTelephony
234      * @see #checkDeviceSupportsMobileData()
235      */
disconnectWifiToConnectToMobile()236     private void disconnectWifiToConnectToMobile() throws InterruptedException {
237         if (mHasWifi && mWifiManager.isWifiEnabled()) {
238             ConnectivityActionReceiver connectMobileReceiver =
239                     new ConnectivityActionReceiver(ConnectivityManager.TYPE_MOBILE,
240                             NetworkInfo.State.CONNECTED);
241             ConnectivityActionReceiver disconnectWifiReceiver =
242                     new ConnectivityActionReceiver(ConnectivityManager.TYPE_WIFI,
243                             NetworkInfo.State.DISCONNECTED);
244             IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
245             mContext.registerReceiver(connectMobileReceiver, filter);
246             mContext.registerReceiver(disconnectWifiReceiver, filter);
247 
248             assertTrue(mWifiManager.setWifiEnabled(false));
249             assertTrue("Failure disconnecting from WiFi.",
250                     disconnectWifiReceiver.waitForStateChange());
251             assertTrue("Device must have access to a metered network for this test.",
252                     connectMobileReceiver.waitForStateChange());
253 
254             mContext.unregisterReceiver(connectMobileReceiver);
255             mContext.unregisterReceiver(disconnectWifiReceiver);
256         }
257     }
258 
259     /** Capture the last connectivity change's network type and state. */
260     private class ConnectivityActionReceiver extends BroadcastReceiver {
261 
262         private final CountDownLatch mReceiveLatch = new CountDownLatch(1);
263 
264         private final int mNetworkType;
265 
266         private final NetworkInfo.State mExpectedState;
267 
ConnectivityActionReceiver(int networkType, NetworkInfo.State expectedState)268         ConnectivityActionReceiver(int networkType, NetworkInfo.State expectedState) {
269             mNetworkType = networkType;
270             mExpectedState = expectedState;
271         }
272 
onReceive(Context context, Intent intent)273         public void onReceive(Context context, Intent intent) {
274             // Dealing with a connectivity changed event for this network type.
275             final int networkTypeChanged =
276                     intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, -1);
277             if (networkTypeChanged == -1) {
278                 Log.e(TAG, "No network type provided in intent");
279                 return;
280             }
281 
282             if (networkTypeChanged != mNetworkType) {
283                 // Only track changes for the connectivity event that we are interested in.
284                 return;
285             }
286             // Pull out the NetworkState object that we're interested in. Necessary because
287             // the ConnectivityManager will filter on uid for background connectivity.
288             NetworkInfo[] allNetworkInfo = mCm.getAllNetworkInfo();
289             NetworkInfo networkInfo = null;
290             for (int i=0; i<allNetworkInfo.length; i++) {
291                 NetworkInfo ni = allNetworkInfo[i];
292                 if (ni.getType() == mNetworkType) {
293                     networkInfo =  ni;
294                     break;
295                 }
296             }
297             if (networkInfo == null) {
298                 Log.e(TAG, "Could not find correct network type.");
299                 return;
300             }
301 
302             NetworkInfo.State networkState = networkInfo.getState();
303             Log.i(TAG, "Network type: " + mNetworkType + " State: " + networkState);
304             if (networkState == mExpectedState) {
305                 mReceiveLatch.countDown();
306             }
307         }
308 
waitForStateChange()309         public boolean waitForStateChange() throws InterruptedException {
310             return mReceiveLatch.await(30, TimeUnit.SECONDS) || hasExpectedState();
311         }
312 
hasExpectedState()313         private boolean hasExpectedState() {
314             return mExpectedState == mCm.getNetworkInfo(mNetworkType).getState();
315         }
316     }
317 
318 }
319