1 /* 2 * Copyright (C) 2019 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 import static android.app.ActivityManager.getCapabilitiesSummary; 19 import static android.app.ActivityManager.procStateToString; 20 import static android.jobscheduler.cts.jobtestapp.TestJobSchedulerReceiver.ACTION_JOB_SCHEDULE_RESULT; 21 import static android.jobscheduler.cts.jobtestapp.TestJobSchedulerReceiver.EXTRA_REQUEST_JOB_UID_STATE; 22 import static android.jobscheduler.cts.jobtestapp.TestJobService.ACTION_JOB_STARTED; 23 import static android.jobscheduler.cts.jobtestapp.TestJobService.ACTION_JOB_STOPPED; 24 import static android.jobscheduler.cts.jobtestapp.TestJobService.INVALID_ADJ; 25 import static android.jobscheduler.cts.jobtestapp.TestJobService.JOB_CAPABILITIES_KEY; 26 import static android.jobscheduler.cts.jobtestapp.TestJobService.JOB_OOM_SCORE_ADJ_KEY; 27 import static android.jobscheduler.cts.jobtestapp.TestJobService.JOB_PARAMS_EXTRA_KEY; 28 import static android.jobscheduler.cts.jobtestapp.TestJobService.JOB_PROC_STATE_KEY; 29 import static android.server.wm.WindowManagerState.STATE_RESUMED; 30 31 import static org.junit.Assert.assertEquals; 32 import static org.junit.Assert.fail; 33 34 import android.app.ActivityManager; 35 import android.app.AppOpsManager; 36 import android.app.job.JobParameters; 37 import android.app.job.JobScheduler; 38 import android.content.BroadcastReceiver; 39 import android.content.ComponentName; 40 import android.content.Context; 41 import android.content.Intent; 42 import android.content.IntentFilter; 43 import android.jobscheduler.cts.jobtestapp.TestActivity; 44 import android.jobscheduler.cts.jobtestapp.TestFgsService; 45 import android.jobscheduler.cts.jobtestapp.TestJobSchedulerReceiver; 46 import android.os.SystemClock; 47 import android.os.UserHandle; 48 import android.server.wm.WindowManagerStateHelper; 49 import android.util.Log; 50 import android.util.SparseArray; 51 52 import com.android.compatibility.common.util.AppOpsUtils; 53 import com.android.compatibility.common.util.AppStandbyUtils; 54 import com.android.compatibility.common.util.CallbackAsserter; 55 import com.android.compatibility.common.util.SystemUtil; 56 57 import java.util.Map; 58 59 /** 60 * Common functions to interact with the test app. 61 */ 62 class TestAppInterface implements AutoCloseable { 63 private static final String TAG = TestAppInterface.class.getSimpleName(); 64 65 static final String TEST_APP_PACKAGE = "android.jobscheduler.cts.jobtestapp"; 66 private static final String TEST_APP_ACTIVITY = TEST_APP_PACKAGE + ".TestActivity"; 67 private static final String TEST_APP_FGS = TEST_APP_PACKAGE + ".TestFgsService"; 68 static final String TEST_APP_RECEIVER = TEST_APP_PACKAGE + ".TestJobSchedulerReceiver"; 69 70 private final Context mContext; 71 private final int mJobId; 72 73 /* accesses must be synchronized on itself */ 74 private final SparseArray<TestJobState> mTestJobStates = new SparseArray(); 75 TestAppInterface(Context ctx, int jobId)76 TestAppInterface(Context ctx, int jobId) { 77 mContext = ctx; 78 mJobId = jobId; 79 80 final IntentFilter intentFilter = new IntentFilter(); 81 intentFilter.addAction(ACTION_JOB_STARTED); 82 intentFilter.addAction(ACTION_JOB_STOPPED); 83 intentFilter.addAction(ACTION_JOB_SCHEDULE_RESULT); 84 mContext.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_EXPORTED_UNAUDITED); 85 SystemUtil.runShellCommand( 86 "am compat enable --no-kill ALLOW_TEST_API_ACCESS " + TEST_APP_PACKAGE); 87 if (AppStandbyUtils.isAppStandbyEnabled()) { 88 // Disable the bucket elevation so that we put the app in lower buckets. 89 SystemUtil.runShellCommand( 90 "am compat enable --no-kill SCHEDULE_EXACT_ALARM_DOES_NOT_ELEVATE_BUCKET " 91 + TEST_APP_PACKAGE); 92 // Force the test app out of the never bucket. 93 SystemUtil.runShellCommand("am set-standby-bucket " + TEST_APP_PACKAGE + " rare"); 94 } 95 } 96 cleanup()97 void cleanup() throws Exception { 98 final Intent cancelJobsIntent = new Intent(TestJobSchedulerReceiver.ACTION_CANCEL_JOBS); 99 cancelJobsIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER)); 100 cancelJobsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 101 mContext.sendBroadcast(cancelJobsIntent); 102 closeActivity(); 103 stopFgs(); 104 mContext.unregisterReceiver(mReceiver); 105 AppOpsUtils.reset(TEST_APP_PACKAGE); 106 SystemUtil.runShellCommand("am compat reset-all " + TEST_APP_PACKAGE); 107 mTestJobStates.clear(); 108 forceStopApp(); // Clean up as much internal/temporary system state as possible 109 } 110 111 @Override close()112 public void close() throws Exception { 113 cleanup(); 114 } 115 scheduleJob(boolean allowWhileIdle, int requiredNetworkType, boolean asExpeditedJob)116 void scheduleJob(boolean allowWhileIdle, int requiredNetworkType, boolean asExpeditedJob) 117 throws Exception { 118 scheduleJob(allowWhileIdle, requiredNetworkType, asExpeditedJob, false); 119 } 120 scheduleJob(boolean allowWhileIdle, int requiredNetworkType, boolean asExpeditedJob, boolean asUserInitiatedJob)121 void scheduleJob(boolean allowWhileIdle, int requiredNetworkType, boolean asExpeditedJob, 122 boolean asUserInitiatedJob) throws Exception { 123 scheduleJob( 124 Map.of( 125 TestJobSchedulerReceiver.EXTRA_ALLOW_IN_IDLE, allowWhileIdle, 126 TestJobSchedulerReceiver.EXTRA_AS_EXPEDITED, asExpeditedJob, 127 TestJobSchedulerReceiver.EXTRA_AS_USER_INITIATED, asUserInitiatedJob 128 ), 129 Map.of( 130 TestJobSchedulerReceiver.EXTRA_REQUIRED_NETWORK_TYPE, requiredNetworkType 131 )); 132 } 133 generateScheduleJobIntent(Map<String, Boolean> booleanExtras, Map<String, Integer> intExtras)134 private Intent generateScheduleJobIntent(Map<String, Boolean> booleanExtras, 135 Map<String, Integer> intExtras) { 136 final Intent scheduleJobIntent = new Intent(TestJobSchedulerReceiver.ACTION_SCHEDULE_JOB); 137 scheduleJobIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 138 if (!intExtras.containsKey(TestJobSchedulerReceiver.EXTRA_JOB_ID_KEY)) { 139 scheduleJobIntent.putExtra(TestJobSchedulerReceiver.EXTRA_JOB_ID_KEY, mJobId); 140 } 141 booleanExtras.forEach(scheduleJobIntent::putExtra); 142 intExtras.forEach(scheduleJobIntent::putExtra); 143 scheduleJobIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER)); 144 return scheduleJobIntent; 145 } 146 scheduleJob(Map<String, Boolean> booleanExtras, Map<String, Integer> intExtras)147 void scheduleJob(Map<String, Boolean> booleanExtras, Map<String, Integer> intExtras) 148 throws Exception { 149 final Intent scheduleJobIntent = generateScheduleJobIntent(booleanExtras, intExtras); 150 151 final CallbackAsserter resultBroadcastAsserter = CallbackAsserter.forBroadcast( 152 new IntentFilter(TestJobSchedulerReceiver.ACTION_JOB_SCHEDULE_RESULT)); 153 mContext.sendBroadcast(scheduleJobIntent); 154 resultBroadcastAsserter.assertCalled("Didn't get schedule job result broadcast", 155 15 /* 15 seconds */); 156 } 157 postUiInitiatingNotification(Map<String, Boolean> booleanExtras, Map<String, Integer> intExtras)158 void postUiInitiatingNotification(Map<String, Boolean> booleanExtras, 159 Map<String, Integer> intExtras) throws Exception { 160 final Intent intent = generateScheduleJobIntent(booleanExtras, intExtras); 161 intent.setAction(TestJobSchedulerReceiver.ACTION_POST_UI_INITIATING_NOTIFICATION); 162 163 final CallbackAsserter resultBroadcastAsserter = CallbackAsserter.forBroadcast( 164 new IntentFilter(TestJobSchedulerReceiver.ACTION_NOTIFICATION_POSTED)); 165 mContext.sendBroadcast(intent); 166 resultBroadcastAsserter.assertCalled("Didn't get notification posted broadcast", 167 15 /* 15 seconds */); 168 } 169 postFgsStartingAlarm()170 void postFgsStartingAlarm() throws Exception { 171 AppOpsUtils.setOpMode(TEST_APP_PACKAGE, 172 AppOpsManager.OPSTR_SCHEDULE_EXACT_ALARM, AppOpsManager.MODE_ALLOWED); 173 final Intent intent = new Intent(TestJobSchedulerReceiver.ACTION_SCHEDULE_FGS_START_ALARM); 174 intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 175 intent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER)); 176 177 final CallbackAsserter resultBroadcastAsserter = CallbackAsserter.forBroadcast( 178 new IntentFilter(TestJobSchedulerReceiver.ACTION_ALARM_SCHEDULED)); 179 mContext.sendBroadcast(intent); 180 resultBroadcastAsserter.assertCalled("Didn't get alarm scheduled broadcast", 181 15 /* 15 seconds */); 182 } 183 184 /** Asks (not forces) JobScheduler to run the job if constraints are met. */ runSatisfiedJob()185 void runSatisfiedJob() throws Exception { 186 SystemUtil.runShellCommand("cmd jobscheduler run -s" 187 + " -u " + UserHandle.myUserId() + " " + TEST_APP_PACKAGE + " " + mJobId); 188 } 189 190 /** Forces JobScheduler to run the job */ forceRunJob()191 void forceRunJob() throws Exception { 192 SystemUtil.runShellCommand("cmd jobscheduler run -f" 193 + " -u " + UserHandle.myUserId() + " " + TEST_APP_PACKAGE + " " + mJobId); 194 } 195 stopJob(int stopReason, int internalStopReason)196 void stopJob(int stopReason, int internalStopReason) throws Exception { 197 SystemUtil.runShellCommand("cmd jobscheduler stop" 198 + " -u " + UserHandle.myUserId() 199 + " -s " + stopReason + " -i " + internalStopReason 200 + " " + TEST_APP_PACKAGE + " " + mJobId); 201 } 202 forceStopApp()203 void forceStopApp() { 204 SystemUtil.runShellCommand("am force-stop" 205 + " --user " + UserHandle.myUserId() + " " + TEST_APP_PACKAGE); 206 } 207 setTestPackageRestricted(boolean restricted)208 void setTestPackageRestricted(boolean restricted) throws Exception { 209 AppOpsUtils.setOpMode(TEST_APP_PACKAGE, "RUN_ANY_IN_BACKGROUND", 210 restricted ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED); 211 } 212 cancelJob()213 void cancelJob() throws Exception { 214 SystemUtil.runShellCommand("cmd jobscheduler cancel" 215 + " -u " + UserHandle.myUserId() + " " + TEST_APP_PACKAGE + " " + mJobId); 216 } 217 startAndKeepTestActivity()218 void startAndKeepTestActivity() { 219 startAndKeepTestActivity(false); 220 } 221 startAndKeepTestActivity(boolean waitForResume)222 void startAndKeepTestActivity(boolean waitForResume) { 223 final Intent testActivity = new Intent(); 224 testActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 225 ComponentName testComponentName = new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY); 226 testActivity.setComponent(testComponentName); 227 mContext.startActivity(testActivity); 228 if (waitForResume) { 229 new WindowManagerStateHelper().waitForActivityState(testComponentName, STATE_RESUMED); 230 } 231 } 232 closeActivity()233 void closeActivity() { 234 closeActivity(false); 235 } 236 closeActivity(boolean waitForClose)237 void closeActivity(boolean waitForClose) { 238 mContext.sendBroadcast(new Intent(TestActivity.ACTION_FINISH_ACTIVITY)); 239 if (waitForClose) { 240 ComponentName testComponentName = 241 new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY); 242 new WindowManagerStateHelper().waitForActivityRemoved(testComponentName); 243 } 244 } 245 startFgs()246 void startFgs() throws Exception { 247 final Intent intent = new Intent(TestJobSchedulerReceiver.ACTION_START_FGS); 248 intent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER)); 249 250 final CallbackAsserter resultBroadcastAsserter = 251 CallbackAsserter.forBroadcast(new IntentFilter(TestFgsService.ACTION_FGS_STARTED)); 252 mContext.sendBroadcast(intent); 253 resultBroadcastAsserter.assertCalled("Didn't get FGS started broadcast", 254 15 /* 15 seconds */); 255 } 256 stopFgs()257 void stopFgs() { 258 final Intent testFgs = new Intent(TestFgsService.ACTION_STOP_FOREGROUND); 259 mContext.sendBroadcast(testFgs); 260 } 261 262 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 263 @Override 264 public void onReceive(Context context, Intent intent) { 265 Log.d(TAG, "Received action " + intent.getAction()); 266 switch (intent.getAction()) { 267 case ACTION_JOB_STARTED: 268 case ACTION_JOB_STOPPED: 269 final JobParameters params = intent.getParcelableExtra(JOB_PARAMS_EXTRA_KEY); 270 Log.d(TAG, "JobId: " + params.getJobId()); 271 synchronized (mTestJobStates) { 272 TestJobState jobState = mTestJobStates.get(params.getJobId()); 273 if (jobState == null) { 274 jobState = new TestJobState(); 275 mTestJobStates.put(params.getJobId(), jobState); 276 } else { 277 jobState.reset(); 278 } 279 jobState.running = ACTION_JOB_STARTED.equals(intent.getAction()); 280 jobState.params = params; 281 // With these broadcasts, the job is/was running, and therefore scheduling 282 // was successful. 283 jobState.scheduleResult = JobScheduler.RESULT_SUCCESS; 284 if (intent.getBooleanExtra(EXTRA_REQUEST_JOB_UID_STATE, false)) { 285 jobState.procState = intent.getIntExtra(JOB_PROC_STATE_KEY, 286 ActivityManager.PROCESS_STATE_NONEXISTENT); 287 jobState.capabilities = intent.getIntExtra(JOB_CAPABILITIES_KEY, 288 ActivityManager.PROCESS_CAPABILITY_NONE); 289 jobState.oomScoreAdj = intent.getIntExtra(JOB_OOM_SCORE_ADJ_KEY, 290 INVALID_ADJ); 291 } 292 } 293 break; 294 case ACTION_JOB_SCHEDULE_RESULT: 295 synchronized (mTestJobStates) { 296 final int jobId = intent.getIntExtra( 297 TestJobSchedulerReceiver.EXTRA_JOB_ID_KEY, 0); 298 TestJobState jobState = mTestJobStates.get(jobId); 299 if (jobState == null) { 300 jobState = new TestJobState(); 301 mTestJobStates.put(jobId, jobState); 302 } else { 303 jobState.reset(); 304 } 305 jobState.running = false; 306 jobState.params = null; 307 jobState.scheduleResult = intent.getIntExtra( 308 TestJobSchedulerReceiver.EXTRA_SCHEDULE_RESULT, -1); 309 } 310 break; 311 } 312 } 313 }; 314 awaitJobStart(long maxWait)315 boolean awaitJobStart(long maxWait) throws Exception { 316 return awaitJobStart(mJobId, maxWait); 317 } 318 awaitJobStart(int jobId, long maxWait)319 boolean awaitJobStart(int jobId, long maxWait) throws Exception { 320 return waitUntilTrue(maxWait, () -> { 321 synchronized (mTestJobStates) { 322 TestJobState jobState = mTestJobStates.get(jobId); 323 return jobState != null && jobState.running; 324 } 325 }); 326 } 327 328 boolean awaitJobStop(long maxWait) throws Exception { 329 return waitUntilTrue(maxWait, () -> { 330 synchronized (mTestJobStates) { 331 TestJobState jobState = mTestJobStates.get(mJobId); 332 return jobState != null && !jobState.running; 333 } 334 }); 335 } 336 337 void assertJobUidState(int procState, int capabilities, int oomScoreAdj) { 338 synchronized (mTestJobStates) { 339 TestJobState jobState = mTestJobStates.get(mJobId); 340 if (jobState == null) { 341 fail("Job not started"); 342 } 343 assertEquals("procState expected=" + procStateToString(procState) 344 + ",actual=" + procStateToString(jobState.procState), 345 procState, jobState.procState); 346 assertEquals("capabilities expected=" + getCapabilitiesSummary(capabilities) 347 + ",actual=" + getCapabilitiesSummary(jobState.capabilities), 348 capabilities, jobState.capabilities); 349 assertEquals("Unexpected oomScoreAdj", oomScoreAdj, jobState.oomScoreAdj); 350 } 351 } 352 353 boolean awaitJobScheduleResult(long maxWaitMs, int jobResult) throws Exception { 354 return awaitJobScheduleResult(mJobId, maxWaitMs, jobResult); 355 } 356 357 boolean awaitJobScheduleResult(int jobId, long maxWaitMs, int jobResult) throws Exception { 358 return waitUntilTrue(maxWaitMs, () -> { 359 synchronized (mTestJobStates) { 360 TestJobState jobState = mTestJobStates.get(jobId); 361 return jobState != null && jobState.scheduleResult == jobResult; 362 } 363 }); 364 } 365 366 private boolean waitUntilTrue(long maxWait, Condition condition) throws Exception { 367 final long deadLine = SystemClock.uptimeMillis() + maxWait; 368 do { 369 Thread.sleep(500); 370 } while (!condition.isTrue() && SystemClock.uptimeMillis() < deadLine); 371 return condition.isTrue(); 372 } 373 374 JobParameters getLastParams() { 375 synchronized (mTestJobStates) { 376 TestJobState jobState = mTestJobStates.get(mJobId); 377 return jobState == null ? null : jobState.params; 378 } 379 } 380 381 private static final class TestJobState { 382 int scheduleResult; 383 boolean running; 384 int procState; 385 int capabilities; 386 int oomScoreAdj; 387 JobParameters params; 388 389 TestJobState() { 390 initState(); 391 } 392 393 private void reset() { 394 initState(); 395 } 396 397 private void initState() { 398 running = false; 399 procState = ActivityManager.PROCESS_STATE_NONEXISTENT; 400 capabilities = ActivityManager.PROCESS_CAPABILITY_NONE; 401 oomScoreAdj = INVALID_ADJ; 402 scheduleResult = -1; 403 } 404 } 405 406 private interface Condition { 407 boolean isTrue() throws Exception; 408 } 409 } 410