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 com.android.server.job;
18 
19 import static com.android.servicestests.apps.jobtestapp.TestJobService.ACTION_JOB_STARTED;
20 import static com.android.servicestests.apps.jobtestapp.TestJobService.ACTION_JOB_STOPPED;
21 import static com.android.servicestests.apps.jobtestapp.TestJobService.JOB_PARAMS_EXTRA_KEY;
22 
23 import static org.junit.Assert.assertFalse;
24 import static org.junit.Assert.assertTrue;
25 
26 import android.app.ActivityManager;
27 import android.app.AppOpsManager;
28 import android.app.IActivityManager;
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.os.IDeviceIdleController;
36 import android.os.RemoteException;
37 import android.os.ServiceManager;
38 import android.os.SystemClock;
39 import android.os.UserHandle;
40 import android.util.Log;
41 
42 import androidx.test.InstrumentationRegistry;
43 import androidx.test.filters.FlakyTest;
44 import androidx.test.filters.LargeTest;
45 import androidx.test.runner.AndroidJUnit4;
46 
47 import com.android.servicestests.apps.jobtestapp.TestJobActivity;
48 
49 import org.junit.After;
50 import org.junit.Before;
51 import org.junit.Test;
52 import org.junit.runner.RunWith;
53 
54 /**
55  * Tests that background restrictions on jobs work as expected.
56  * This test requires test-apps/JobTestApp to be installed on the device.
57  * To run this test from root of checkout:
58  * <pre>
59  *  mmm -j32 frameworks/base/services/tests/servicestests/
60  *  adb install -r $OUT/data/app/JobTestApp/JobTestApp.apk
61  *  adb install -r $OUT/data/app/FrameworksServicesTests/FrameworksServicesTests.apk
62  *  adb  shell am instrument -e class 'com.android.server.job.BackgroundRestrictionsTest' -w \
63  *  com.android.frameworks.servicestests
64  * </pre>
65  */
66 @RunWith(AndroidJUnit4.class)
67 @LargeTest
68 public class BackgroundRestrictionsTest {
69     private static final String TAG = BackgroundRestrictionsTest.class.getSimpleName();
70     private static final String TEST_APP_PACKAGE = "com.android.servicestests.apps.jobtestapp";
71     private static final String TEST_APP_ACTIVITY = TEST_APP_PACKAGE + ".TestJobActivity";
72     private static final long POLL_INTERVAL = 500;
73     private static final long DEFAULT_WAIT_TIMEOUT = 10_000;
74 
75     private Context mContext;
76     private AppOpsManager mAppOpsManager;
77     private IDeviceIdleController mDeviceIdleController;
78     private IActivityManager mIActivityManager;
79     private volatile int mTestJobId = -1;
80     private int mTestPackageUid;
81     /* accesses must be synchronized on itself */
82     private final TestJobStatus mTestJobStatus = new TestJobStatus();
83     private final BroadcastReceiver mJobStateChangeReceiver = new BroadcastReceiver() {
84         @Override
85         public void onReceive(Context context, Intent intent) {
86             final JobParameters params = intent.getParcelableExtra(JOB_PARAMS_EXTRA_KEY);
87             Log.d(TAG, "Received action " + intent.getAction());
88             synchronized (mTestJobStatus) {
89                 switch (intent.getAction()) {
90                     case ACTION_JOB_STARTED:
91                         mTestJobStatus.running = true;
92                         mTestJobStatus.jobId = params.getJobId();
93                         mTestJobStatus.stopReason = JobParameters.STOP_REASON_UNDEFINED;
94                         break;
95                     case ACTION_JOB_STOPPED:
96                         mTestJobStatus.running = false;
97                         mTestJobStatus.jobId = params.getJobId();
98                         mTestJobStatus.stopReason = params.getStopReason();
99                         break;
100                 }
101             }
102         }
103     };
104 
105     @Before
setUp()106     public void setUp() throws Exception {
107         mContext = InstrumentationRegistry.getTargetContext();
108         mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
109         mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
110                 ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
111         mIActivityManager = ActivityManager.getService();
112         mTestPackageUid = mContext.getPackageManager().getPackageUid(TEST_APP_PACKAGE, 0);
113         mTestJobStatus.reset();
114         final IntentFilter intentFilter = new IntentFilter();
115         intentFilter.addAction(ACTION_JOB_STARTED);
116         intentFilter.addAction(ACTION_JOB_STOPPED);
117         mContext.registerReceiver(mJobStateChangeReceiver, intentFilter,
118                 Context.RECEIVER_EXPORTED_UNAUDITED);
119         setAppOpsModeAllowed(true);
120         setPowerExemption(false);
121     }
122 
scheduleTestJob()123     private void scheduleTestJob() {
124         mTestJobId = (int) (SystemClock.uptimeMillis() / 1000);
125         final Intent scheduleJobIntent = new Intent(TestJobActivity.ACTION_START_JOB);
126         scheduleJobIntent.putExtra(TestJobActivity.EXTRA_JOB_ID_KEY, mTestJobId);
127         scheduleJobIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
128         scheduleJobIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY));
129         mContext.startActivity(scheduleJobIntent);
130     }
131 
scheduleAndAssertJobStarted()132     private void scheduleAndAssertJobStarted() throws Exception {
133         scheduleTestJob();
134         Thread.sleep(TestJobActivity.JOB_MINIMUM_LATENCY);
135         assertTrue("Job did not start after scheduling", awaitJobStart(DEFAULT_WAIT_TIMEOUT));
136     }
137 
138     @FlakyTest
139     @Test
testPowerExemption()140     public void testPowerExemption() throws Exception {
141         scheduleAndAssertJobStarted();
142         setAppOpsModeAllowed(false);
143         mIActivityManager.makePackageIdle(TEST_APP_PACKAGE, UserHandle.USER_CURRENT);
144         assertTrue("Job did not stop after putting app under bg-restriction",
145                 awaitJobStop(DEFAULT_WAIT_TIMEOUT,
146                         JobParameters.STOP_REASON_BACKGROUND_RESTRICTION));
147 
148         setPowerExemption(true);
149         scheduleTestJob();
150         Thread.sleep(TestJobActivity.JOB_MINIMUM_LATENCY);
151         assertTrue("Job did not start when the app was in the power exemption list",
152                 awaitJobStart(DEFAULT_WAIT_TIMEOUT));
153 
154         setPowerExemption(false);
155         assertTrue("Job did not stop after removing from the power exemption list",
156                 awaitJobStop(DEFAULT_WAIT_TIMEOUT,
157                         JobParameters.STOP_REASON_BACKGROUND_RESTRICTION));
158 
159         scheduleTestJob();
160         Thread.sleep(TestJobActivity.JOB_MINIMUM_LATENCY);
161         assertFalse("Job started under bg-restrictions", awaitJobStart(DEFAULT_WAIT_TIMEOUT));
162         setPowerExemption(true);
163         assertTrue("Job did not start when the app was in the power exemption list",
164                 awaitJobStart(DEFAULT_WAIT_TIMEOUT));
165     }
166 
167     @After
tearDown()168     public void tearDown() throws Exception {
169         final Intent cancelJobsIntent = new Intent(TestJobActivity.ACTION_CANCEL_JOBS);
170         cancelJobsIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY));
171         cancelJobsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
172         mContext.startActivity(cancelJobsIntent);
173         mContext.unregisterReceiver(mJobStateChangeReceiver);
174         Thread.sleep(500); // To avoid race with register in the next setUp
175         setAppOpsModeAllowed(true);
176         setPowerExemption(false);
177     }
178 
setPowerExemption(boolean exempt)179     private void setPowerExemption(boolean exempt) throws RemoteException {
180         if (exempt) {
181             mDeviceIdleController.addPowerSaveWhitelistApp(TEST_APP_PACKAGE);
182         } else {
183             mDeviceIdleController.removePowerSaveWhitelistApp(TEST_APP_PACKAGE);
184         }
185     }
186 
setAppOpsModeAllowed(boolean allow)187     private void setAppOpsModeAllowed(boolean allow) {
188         mAppOpsManager.setMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mTestPackageUid,
189                 TEST_APP_PACKAGE, allow ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED);
190     }
191 
awaitJobStart(long timeout)192     private boolean awaitJobStart(long timeout) throws InterruptedException {
193         return waitUntilTrue(timeout, () -> {
194             synchronized (mTestJobStatus) {
195                 return (mTestJobStatus.jobId == mTestJobId) && mTestJobStatus.running;
196             }
197         });
198     }
199 
200     private boolean awaitJobStop(long timeout, @JobParameters.StopReason int expectedStopReason)
201             throws InterruptedException {
202         return waitUntilTrue(timeout, () -> {
203             synchronized (mTestJobStatus) {
204                 return (mTestJobStatus.jobId == mTestJobId) && !mTestJobStatus.running
205                         && (expectedStopReason == JobParameters.STOP_REASON_UNDEFINED
206                         || mTestJobStatus.stopReason == expectedStopReason);
207             }
208         });
209     }
210 
211     private boolean waitUntilTrue(long timeout, Condition condition) throws InterruptedException {
212         final long deadLine = SystemClock.uptimeMillis() + timeout;
213         do {
214             Thread.sleep(POLL_INTERVAL);
215         } while (!condition.isTrue() && SystemClock.uptimeMillis() < deadLine);
216         return condition.isTrue();
217     }
218 
219     private static final class TestJobStatus {
220         int jobId;
221         int stopReason;
222         boolean running;
223 
224         private void reset() {
225             running = false;
226             stopReason = jobId = 0;
227         }
228     }
229 
230     private interface Condition {
231         boolean isTrue();
232     }
233 }
234