1 /*
2  * Copyright (C) 2017 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 
17 package android.jobscheduler.cts;
18 
19 import static android.jobscheduler.cts.jobtestapp.TestJobService.ACTION_JOB_STARTED;
20 import static android.jobscheduler.cts.jobtestapp.TestJobService.ACTION_JOB_STOPPED;
21 import static android.jobscheduler.cts.jobtestapp.TestJobService.JOB_PARAMS_EXTRA_KEY;
22 import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
23 import static android.os.PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED;
24 
25 import static org.junit.Assert.assertFalse;
26 import static org.junit.Assert.assertTrue;
27 import static org.junit.Assume.assumeTrue;
28 
29 import android.app.job.JobParameters;
30 import android.content.BroadcastReceiver;
31 import android.content.ComponentName;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.content.IntentFilter;
35 import android.jobscheduler.cts.jobtestapp.TestActivity;
36 import android.jobscheduler.cts.jobtestapp.TestJobSchedulerReceiver;
37 import android.os.PowerManager;
38 import android.os.SystemClock;
39 import android.support.test.InstrumentationRegistry;
40 import android.support.test.filters.LargeTest;
41 import android.support.test.runner.AndroidJUnit4;
42 import android.support.test.uiautomator.UiDevice;
43 import android.util.Log;
44 
45 import com.android.compatibility.common.util.AppStandbyUtils;
46 
47 import org.junit.After;
48 import org.junit.Before;
49 import org.junit.Test;
50 import org.junit.runner.RunWith;
51 
52 /**
53  * Tests that temp whitelisted apps can run jobs if all the other constraints are met
54  */
55 @RunWith(AndroidJUnit4.class)
56 @LargeTest
57 public class DeviceIdleJobsTest {
58     private static final String TAG = DeviceIdleJobsTest.class.getSimpleName();
59     private static final String TEST_APP_PACKAGE = "android.jobscheduler.cts.jobtestapp";
60     private static final String TEST_APP_RECEIVER = TEST_APP_PACKAGE + ".TestJobSchedulerReceiver";
61     private static final String TEST_APP_ACTIVITY = TEST_APP_PACKAGE + ".TestActivity";
62     private static final long BACKGROUND_JOBS_EXPECTED_DELAY = 3_000;
63     private static final long POLL_INTERVAL = 500;
64     private static final long DEFAULT_WAIT_TIMEOUT = 1000;
65     private static final long SHELL_TIMEOUT = 3_000;
66 
67     enum Bucket {
68         ACTIVE,
69         WORKING_SET,
70         FREQUENT,
71         RARE,
72         NEVER
73     }
74 
75     private Context mContext;
76     private UiDevice mUiDevice;
77     private PowerManager mPowerManager;
78     private long mTempWhitelistExpiryElapsed;
79     private int mTestJobId;
80     private int mTestPackageUid;
81     private boolean mDeviceInDoze;
82     private boolean mDeviceIdleEnabled;
83     private boolean mAppStandbyEnabled;
84 
85     /* accesses must be synchronized on itself */
86     private final TestJobStatus mTestJobStatus = new TestJobStatus();
87     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
88         @Override
89         public void onReceive(Context context, Intent intent) {
90             Log.d(TAG, "Received action " + intent.getAction());
91             switch (intent.getAction()) {
92                 case ACTION_JOB_STARTED:
93                 case ACTION_JOB_STOPPED:
94                     final JobParameters params = intent.getParcelableExtra(JOB_PARAMS_EXTRA_KEY);
95                     Log.d(TAG, "JobId: " + params.getJobId());
96                     synchronized (mTestJobStatus) {
97                         mTestJobStatus.running = ACTION_JOB_STARTED.equals(intent.getAction());
98                         mTestJobStatus.jobId = params.getJobId();
99                     }
100                     break;
101                 case ACTION_DEVICE_IDLE_MODE_CHANGED:
102                 case ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED:
103                     synchronized (DeviceIdleJobsTest.this) {
104                         mDeviceInDoze = mPowerManager.isDeviceIdleMode();
105                         Log.d(TAG, "mDeviceInDoze: " + mDeviceInDoze);
106                     }
107                     break;
108             }
109         }
110     };
111 
isDeviceIdleEnabled(UiDevice uiDevice)112     private static boolean isDeviceIdleEnabled(UiDevice uiDevice) throws Exception {
113         final String output = uiDevice.executeShellCommand("cmd deviceidle enabled deep").trim();
114         return Integer.parseInt(output) != 0;
115     }
116 
117     @Before
setUp()118     public void setUp() throws Exception {
119         mContext = InstrumentationRegistry.getTargetContext();
120         mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
121         mPowerManager = mContext.getSystemService(PowerManager.class);
122         mDeviceInDoze = mPowerManager.isDeviceIdleMode();
123         mTestPackageUid = mContext.getPackageManager().getPackageUid(TEST_APP_PACKAGE, 0);
124         mTestJobId = (int) (SystemClock.uptimeMillis() / 1000);
125         mTestJobStatus.reset();
126         mTempWhitelistExpiryElapsed = -1;
127         final IntentFilter intentFilter = new IntentFilter();
128         intentFilter.addAction(ACTION_JOB_STARTED);
129         intentFilter.addAction(ACTION_JOB_STOPPED);
130         intentFilter.addAction(ACTION_DEVICE_IDLE_MODE_CHANGED);
131         intentFilter.addAction(ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED);
132         mContext.registerReceiver(mReceiver, intentFilter);
133         assertFalse("Test package already in temp whitelist", isTestAppTempWhitelisted());
134         makeTestPackageIdle();
135         mDeviceIdleEnabled = isDeviceIdleEnabled(mUiDevice);
136         mAppStandbyEnabled = AppStandbyUtils.isAppStandbyEnabled();
137         if (mAppStandbyEnabled) {
138             setTestPackageStandbyBucket(Bucket.ACTIVE);
139         } else {
140             Log.w(TAG, "App standby not enabled on test device");
141         }
142     }
143 
144     @Test
testAllowWhileIdleJobInTempwhitelist()145     public void testAllowWhileIdleJobInTempwhitelist() throws Exception {
146         assumeTrue("device idle not enabled", mDeviceIdleEnabled);
147 
148         toggleDeviceIdleState(true);
149         Thread.sleep(DEFAULT_WAIT_TIMEOUT);
150         sendScheduleJobBroadcast(true);
151         assertFalse("Job started without being tempwhitelisted", awaitJobStart(5_000));
152         tempWhitelistTestApp(5_000);
153         assertTrue("Job with allow_while_idle flag did not start when the app was tempwhitelisted",
154                 awaitJobStart(DEFAULT_WAIT_TIMEOUT));
155     }
156 
157     @Test
testForegroundJobsStartImmediately()158     public void testForegroundJobsStartImmediately() throws Exception {
159         assumeTrue("device idle not enabled", mDeviceIdleEnabled);
160 
161         sendScheduleJobBroadcast(false);
162         assertTrue("Job did not start after scheduling", awaitJobStart(DEFAULT_WAIT_TIMEOUT));
163         toggleDeviceIdleState(true);
164         assertTrue("Job did not stop on entering doze", awaitJobStop(DEFAULT_WAIT_TIMEOUT));
165         Thread.sleep(TestJobSchedulerReceiver.JOB_INITIAL_BACKOFF);
166         startAndKeepTestActivity();
167         toggleDeviceIdleState(false);
168         assertTrue("Job for foreground app did not start immediately when device exited doze",
169                 awaitJobStart(3_000));
170     }
171 
172     @Test
testBackgroundJobsDelayed()173     public void testBackgroundJobsDelayed() throws Exception {
174         assumeTrue("device idle not enabled", mDeviceIdleEnabled);
175 
176         sendScheduleJobBroadcast(false);
177         assertTrue("Job did not start after scheduling", awaitJobStart(DEFAULT_WAIT_TIMEOUT));
178         toggleDeviceIdleState(true);
179         assertTrue("Job did not stop on entering doze", awaitJobStop(DEFAULT_WAIT_TIMEOUT));
180         Thread.sleep(TestJobSchedulerReceiver.JOB_INITIAL_BACKOFF);
181         toggleDeviceIdleState(false);
182         assertFalse("Job for background app started immediately when device exited doze",
183                 awaitJobStart(DEFAULT_WAIT_TIMEOUT));
184         Thread.sleep(BACKGROUND_JOBS_EXPECTED_DELAY - DEFAULT_WAIT_TIMEOUT);
185         assertTrue("Job for background app did not start after the expected delay of "
186                 + BACKGROUND_JOBS_EXPECTED_DELAY + "ms", awaitJobStart(DEFAULT_WAIT_TIMEOUT));
187     }
188 
189     @Test
testJobsInNeverApp()190     public void testJobsInNeverApp() throws Exception {
191         assumeTrue("app standby not enabled", mAppStandbyEnabled);
192 
193         enterFakeUnpluggedState();
194         setTestPackageStandbyBucket(Bucket.NEVER);
195         Thread.sleep(DEFAULT_WAIT_TIMEOUT);
196         sendScheduleJobBroadcast(false);
197         assertFalse("New job started in NEVER standby", awaitJobStart(3_000));
198         resetFakeUnpluggedState();
199     }
200 
201     @Test
testUidActiveBypassesStandby()202     public void testUidActiveBypassesStandby() throws Exception {
203         enterFakeUnpluggedState();
204         setTestPackageStandbyBucket(Bucket.NEVER);
205         tempWhitelistTestApp(6_000);
206         Thread.sleep(DEFAULT_WAIT_TIMEOUT);
207         sendScheduleJobBroadcast(false);
208         assertTrue("New job in uid-active app failed to start in NEVER standby",
209                 awaitJobStart(4_000));
210         resetFakeUnpluggedState();
211     }
212 
213     @After
tearDown()214     public void tearDown() throws Exception {
215         if (mDeviceIdleEnabled) {
216             toggleDeviceIdleState(false);
217         }
218         final Intent cancelJobsIntent = new Intent(TestJobSchedulerReceiver.ACTION_CANCEL_JOBS);
219         cancelJobsIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER));
220         cancelJobsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
221         mContext.sendBroadcast(cancelJobsIntent);
222         mContext.sendBroadcast(new Intent(TestActivity.ACTION_FINISH_ACTIVITY));
223         mContext.unregisterReceiver(mReceiver);
224         Thread.sleep(500); // To avoid any race between unregister and the next register in setUp
225         waitUntilTestAppNotInTempWhitelist();
226     }
227 
isTestAppTempWhitelisted()228     private boolean isTestAppTempWhitelisted() throws Exception {
229         final String output = mUiDevice.executeShellCommand("cmd deviceidle tempwhitelist").trim();
230         for (String line : output.split("\n")) {
231             if (line.contains("UID="+mTestPackageUid)) {
232                 return true;
233             }
234         }
235         return false;
236     }
237 
startAndKeepTestActivity()238     private void startAndKeepTestActivity() {
239         final Intent testActivity = new Intent();
240         testActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
241         testActivity.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY));
242         mContext.startActivity(testActivity);
243     }
244 
sendScheduleJobBroadcast(boolean allowWhileIdle)245     private void sendScheduleJobBroadcast(boolean allowWhileIdle) throws Exception {
246         final Intent scheduleJobIntent = new Intent(TestJobSchedulerReceiver.ACTION_SCHEDULE_JOB);
247         scheduleJobIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
248         scheduleJobIntent.putExtra(TestJobSchedulerReceiver.EXTRA_JOB_ID_KEY, mTestJobId);
249         scheduleJobIntent.putExtra(TestJobSchedulerReceiver.EXTRA_ALLOW_IN_IDLE, allowWhileIdle);
250         scheduleJobIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER));
251         mContext.sendBroadcast(scheduleJobIntent);
252     }
253 
toggleDeviceIdleState(final boolean idle)254     private void toggleDeviceIdleState(final boolean idle) throws Exception {
255         mUiDevice.executeShellCommand("cmd deviceidle " + (idle ? "force-idle" : "unforce"));
256         assertTrue("Could not change device idle state to " + idle,
257                 waitUntilTrue(SHELL_TIMEOUT, () -> {
258                     synchronized (DeviceIdleJobsTest.this) {
259                         return mDeviceInDoze == idle;
260                     }
261                 }));
262     }
263 
tempWhitelistTestApp(long duration)264     private void tempWhitelistTestApp(long duration) throws Exception {
265         mUiDevice.executeShellCommand("cmd deviceidle tempwhitelist -d " + duration
266                 + " " + TEST_APP_PACKAGE);
267         mTempWhitelistExpiryElapsed = SystemClock.elapsedRealtime() + duration;
268     }
269 
makeTestPackageIdle()270     private void makeTestPackageIdle() throws Exception {
271         mUiDevice.executeShellCommand("am make-uid-idle --user current " + TEST_APP_PACKAGE);
272     }
273 
setTestPackageStandbyBucket(Bucket bucket)274     private void setTestPackageStandbyBucket(Bucket bucket) throws Exception {
275         final String bucketName;
276         switch (bucket) {
277             case ACTIVE: bucketName = "active"; break;
278             case WORKING_SET: bucketName = "working"; break;
279             case FREQUENT: bucketName = "frequent"; break;
280             case RARE: bucketName = "rare"; break;
281             case NEVER: bucketName = "never"; break;
282             default:
283                 throw new IllegalArgumentException("Requested unknown bucket " + bucket);
284         }
285         mUiDevice.executeShellCommand("am set-standby-bucket " + TEST_APP_PACKAGE
286                 + " " + bucketName);
287     }
288 
enterFakeUnpluggedState()289     private void enterFakeUnpluggedState() throws Exception {
290         mUiDevice.executeShellCommand("dumpsys battery unplug");
291     }
292 
resetFakeUnpluggedState()293     private void resetFakeUnpluggedState() throws Exception  {
294         mUiDevice.executeShellCommand("dumpsys battery reset");
295     }
296 
waitUntilTestAppNotInTempWhitelist()297     private boolean waitUntilTestAppNotInTempWhitelist() throws Exception {
298         long now;
299         boolean interrupted = false;
300         while ((now = SystemClock.elapsedRealtime()) < mTempWhitelistExpiryElapsed) {
301             try {
302                 Thread.sleep(mTempWhitelistExpiryElapsed - now);
303             } catch (InterruptedException iexc) {
304                 interrupted = true;
305             }
306         }
307         if (interrupted) {
308             Thread.currentThread().interrupt();
309         }
310         return waitUntilTrue(SHELL_TIMEOUT, () -> !isTestAppTempWhitelisted());
311     }
312 
awaitJobStart(long maxWait)313     private boolean awaitJobStart(long maxWait) throws Exception {
314         return waitUntilTrue(maxWait, () -> {
315             synchronized (mTestJobStatus) {
316                 return (mTestJobStatus.jobId == mTestJobId) && mTestJobStatus.running;
317             }
318         });
319     }
320 
321     private boolean awaitJobStop(long maxWait) throws Exception {
322         return waitUntilTrue(maxWait, () -> {
323             synchronized (mTestJobStatus) {
324                 return (mTestJobStatus.jobId == mTestJobId) && !mTestJobStatus.running;
325             }
326         });
327     }
328 
329     private boolean waitUntilTrue(long maxWait, Condition condition) throws Exception {
330         final long deadLine = SystemClock.uptimeMillis() + maxWait;
331         do {
332             Thread.sleep(POLL_INTERVAL);
333         } while (!condition.isTrue() && SystemClock.uptimeMillis() < deadLine);
334         return condition.isTrue();
335     }
336 
337     private static final class TestJobStatus {
338         int jobId;
339         boolean running;
340         private void reset() {
341             running = false;
342         }
343     }
344 
345     private interface Condition {
346         boolean isTrue() throws Exception;
347     }
348 }
349