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