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