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 17 package com.android.server.job; 18 19 import static android.text.format.DateUtils.DAY_IN_MILLIS; 20 import static android.text.format.DateUtils.HOUR_IN_MILLIS; 21 import static android.text.format.DateUtils.MINUTE_IN_MILLIS; 22 23 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; 24 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; 25 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; 26 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; 27 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; 28 import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; 29 import static com.android.server.job.JobSchedulerService.RARE_INDEX; 30 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; 31 32 import static org.junit.Assert.assertEquals; 33 import static org.junit.Assert.assertTrue; 34 import static org.junit.Assert.fail; 35 import static org.mockito.ArgumentMatchers.any; 36 import static org.mockito.ArgumentMatchers.anyInt; 37 import static org.mockito.Mockito.when; 38 39 import android.app.ActivityManager; 40 import android.app.ActivityManagerInternal; 41 import android.app.IActivityManager; 42 import android.app.UiModeManager; 43 import android.app.job.JobInfo; 44 import android.app.job.JobScheduler; 45 import android.app.usage.UsageStatsManagerInternal; 46 import android.content.ComponentName; 47 import android.content.Context; 48 import android.content.pm.PackageManager; 49 import android.content.pm.PackageManagerInternal; 50 import android.content.res.Resources; 51 import android.net.ConnectivityManager; 52 import android.net.NetworkPolicyManager; 53 import android.os.BatteryManagerInternal; 54 import android.os.Looper; 55 import android.os.RemoteException; 56 import android.os.ServiceManager; 57 import android.os.SystemClock; 58 import android.util.Log; 59 import android.util.SparseBooleanArray; 60 import android.util.SparseLongArray; 61 62 import com.android.server.AppStateTracker; 63 import com.android.server.AppStateTrackerImpl; 64 import com.android.server.DeviceIdleInternal; 65 import com.android.server.LocalServices; 66 import com.android.server.PowerAllowlistInternal; 67 import com.android.server.SystemServiceManager; 68 import com.android.server.job.controllers.JobStatus; 69 import com.android.server.pm.UserManagerInternal; 70 import com.android.server.usage.AppStandbyInternal; 71 72 import org.junit.After; 73 import org.junit.Before; 74 import org.junit.Test; 75 import org.mockito.Mock; 76 import org.mockito.MockitoSession; 77 import org.mockito.quality.Strictness; 78 79 import java.time.Clock; 80 import java.time.Duration; 81 import java.time.ZoneOffset; 82 import java.util.Random; 83 84 public class JobSchedulerServiceTest { 85 private static final String TAG = JobSchedulerServiceTest.class.getSimpleName(); 86 87 private JobSchedulerService mService; 88 89 private MockitoSession mMockingSession; 90 @Mock 91 private ActivityManagerInternal mActivityMangerInternal; 92 @Mock 93 private Context mContext; 94 95 private class TestJobSchedulerService extends JobSchedulerService { TestJobSchedulerService(Context context)96 TestJobSchedulerService(Context context) { 97 super(context); 98 mAppStateTracker = mock(AppStateTrackerImpl.class); 99 } 100 101 @Override isChainedAttributionEnabled()102 public boolean isChainedAttributionEnabled() { 103 return false; 104 } 105 } 106 107 @Before setUp()108 public void setUp() { 109 mMockingSession = mockitoSession() 110 .initMocks(this) 111 .strictness(Strictness.LENIENT) 112 .mockStatic(LocalServices.class) 113 .mockStatic(ServiceManager.class) 114 .startMocking(); 115 116 // Called in JobSchedulerService constructor. 117 when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper()); 118 doReturn(mActivityMangerInternal) 119 .when(() -> LocalServices.getService(ActivityManagerInternal.class)); 120 doReturn(mock(AppStandbyInternal.class)) 121 .when(() -> LocalServices.getService(AppStandbyInternal.class)); 122 doReturn(mock(UsageStatsManagerInternal.class)) 123 .when(() -> LocalServices.getService(UsageStatsManagerInternal.class)); 124 when(mContext.getString(anyInt())).thenReturn("some_test_string"); 125 // Called in BackgroundJobsController constructor. 126 doReturn(mock(AppStateTrackerImpl.class)) 127 .when(() -> LocalServices.getService(AppStateTracker.class)); 128 // Called in BatteryController constructor. 129 doReturn(mock(BatteryManagerInternal.class)) 130 .when(() -> LocalServices.getService(BatteryManagerInternal.class)); 131 // Called in ConnectivityController constructor. 132 when(mContext.getSystemService(ConnectivityManager.class)) 133 .thenReturn(mock(ConnectivityManager.class)); 134 when(mContext.getSystemService(NetworkPolicyManager.class)) 135 .thenReturn(mock(NetworkPolicyManager.class)); 136 // Called in DeviceIdleJobsController constructor. 137 doReturn(mock(DeviceIdleInternal.class)) 138 .when(() -> LocalServices.getService(DeviceIdleInternal.class)); 139 // Used in JobConcurrencyManager. 140 doReturn(mock(UserManagerInternal.class)) 141 .when(() -> LocalServices.getService(UserManagerInternal.class)); 142 // Used in JobStatus. 143 doReturn(mock(PackageManagerInternal.class)) 144 .when(() -> LocalServices.getService(PackageManagerInternal.class)); 145 // Called via IdleController constructor. 146 when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); 147 when(mContext.getResources()).thenReturn(mock(Resources.class)); 148 // Called in QuotaController constructor. 149 doReturn(mock(PowerAllowlistInternal.class)) 150 .when(() -> LocalServices.getService(PowerAllowlistInternal.class)); 151 IActivityManager activityManager = ActivityManager.getService(); 152 spyOn(activityManager); 153 try { 154 doNothing().when(activityManager).registerUidObserver(any(), anyInt(), anyInt(), any()); 155 } catch (RemoteException e) { 156 fail("registerUidObserver threw exception: " + e.getMessage()); 157 } 158 // Called by QuotaTracker 159 doReturn(mock(SystemServiceManager.class)) 160 .when(() -> LocalServices.getService(SystemServiceManager.class)); 161 162 JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC); 163 JobSchedulerService.sElapsedRealtimeClock = 164 Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC); 165 // Called by DeviceIdlenessTracker 166 when(mContext.getSystemService(UiModeManager.class)).thenReturn(mock(UiModeManager.class)); 167 168 mService = new TestJobSchedulerService(mContext); 169 } 170 171 @After tearDown()172 public void tearDown() { 173 if (mMockingSession != null) { 174 mMockingSession.finishMocking(); 175 } 176 } 177 getAdvancedClock(Clock clock, long incrementMs)178 private Clock getAdvancedClock(Clock clock, long incrementMs) { 179 return Clock.offset(clock, Duration.ofMillis(incrementMs)); 180 } 181 advanceElapsedClock(long incrementMs)182 private void advanceElapsedClock(long incrementMs) { 183 JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock( 184 JobSchedulerService.sElapsedRealtimeClock, incrementMs); 185 } 186 createJobInfo()187 private static JobInfo.Builder createJobInfo() { 188 return createJobInfo(351); 189 } 190 createJobInfo(int jobId)191 private static JobInfo.Builder createJobInfo(int jobId) { 192 return new JobInfo.Builder(jobId, new ComponentName("foo", "bar")); 193 } 194 createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder)195 private JobStatus createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder) { 196 return createJobStatus(testTag, jobInfoBuilder, 1234); 197 } 198 createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder, int callingUid)199 private JobStatus createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder, 200 int callingUid) { 201 return JobStatus.createFromJobInfo( 202 jobInfoBuilder.build(), callingUid, "com.android.test", 0, testTag); 203 } 204 205 /** 206 * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job 207 * with the correct delay and deadline constraints if the periodic job is scheduled with the 208 * minimum possible period. 209 */ 210 @Test testGetRescheduleJobForPeriodic_minPeriod()211 public void testGetRescheduleJobForPeriodic_minPeriod() { 212 final long now = sElapsedRealtimeClock.millis(); 213 JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow", 214 createJobInfo().setPeriodic(15 * MINUTE_IN_MILLIS)); 215 final long nextWindowStartTime = now + 15 * MINUTE_IN_MILLIS; 216 final long nextWindowEndTime = now + 30 * MINUTE_IN_MILLIS; 217 218 for (int i = 0; i < 25; i++) { 219 JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job); 220 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 221 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 222 advanceElapsedClock(30_000); // 30 seconds 223 } 224 225 for (int i = 0; i < 5; i++) { 226 // Window buffering in last 1/6 of window. 227 JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job); 228 assertEquals(nextWindowStartTime + i * 30_000, rescheduledJob.getEarliestRunTime()); 229 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 230 advanceElapsedClock(30_000); // 30 seconds 231 } 232 } 233 234 /** 235 * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job 236 * with the correct delay and deadline constraints if the periodic job is scheduled with a 237 * period that's too large. 238 */ 239 @Test testGetRescheduleJobForPeriodic_largePeriod()240 public void testGetRescheduleJobForPeriodic_largePeriod() { 241 final long now = sElapsedRealtimeClock.millis(); 242 JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow", 243 createJobInfo().setPeriodic(2 * 365 * DAY_IN_MILLIS)); 244 assertEquals(now, job.getEarliestRunTime()); 245 // Periods are capped at 365 days (1 year). 246 assertEquals(now + 365 * DAY_IN_MILLIS, job.getLatestRunTimeElapsed()); 247 final long nextWindowStartTime = now + 365 * DAY_IN_MILLIS; 248 final long nextWindowEndTime = nextWindowStartTime + 365 * DAY_IN_MILLIS; 249 250 JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job); 251 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 252 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 253 } 254 255 /** 256 * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job 257 * with the correct delay and deadline constraints if the periodic job is completed and 258 * rescheduled while run in its expected running window. 259 */ 260 @Test testGetRescheduleJobForPeriodic_insideWindow()261 public void testGetRescheduleJobForPeriodic_insideWindow() { 262 final long now = sElapsedRealtimeClock.millis(); 263 JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow", 264 createJobInfo().setPeriodic(HOUR_IN_MILLIS)); 265 final long nextWindowStartTime = now + HOUR_IN_MILLIS; 266 final long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS; 267 268 JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job); 269 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 270 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 271 272 advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 10 minutes 273 274 rescheduledJob = mService.getRescheduleJobForPeriodic(job); 275 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 276 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 277 278 advanceElapsedClock(20 * MINUTE_IN_MILLIS); // now + 30 minutes 279 280 rescheduledJob = mService.getRescheduleJobForPeriodic(job); 281 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 282 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 283 284 advanceElapsedClock(25 * MINUTE_IN_MILLIS); // now + 55 minutes 285 286 rescheduledJob = mService.getRescheduleJobForPeriodic(job); 287 // Shifted because it's close to the end of the window. 288 assertEquals(nextWindowStartTime + 5 * MINUTE_IN_MILLIS, 289 rescheduledJob.getEarliestRunTime()); 290 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 291 292 advanceElapsedClock(4 * MINUTE_IN_MILLIS); // now + 59 minutes 293 294 rescheduledJob = mService.getRescheduleJobForPeriodic(job); 295 // Shifted because it's close to the end of the window. 296 assertEquals(nextWindowStartTime + 9 * MINUTE_IN_MILLIS, 297 rescheduledJob.getEarliestRunTime()); 298 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 299 } 300 301 /** 302 * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job 303 * with an extra delay and correct deadline constraint if the periodic job is completed near the 304 * end of its expected running window. 305 */ 306 @Test testGetRescheduleJobForPeriodic_closeToEndOfWindow()307 public void testGetRescheduleJobForPeriodic_closeToEndOfWindow() { 308 JobStatus frequentJob = createJobStatus( 309 "testGetRescheduleJobForPeriodic_closeToEndOfWindow", 310 createJobInfo().setPeriodic(15 * MINUTE_IN_MILLIS)); 311 long now = sElapsedRealtimeClock.millis(); 312 long nextWindowStartTime = now + 15 * MINUTE_IN_MILLIS; 313 long nextWindowEndTime = now + 30 * MINUTE_IN_MILLIS; 314 315 // At the beginning of the window. Next window should be unaffected. 316 JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(frequentJob); 317 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 318 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 319 320 // Halfway through window. Next window should be unaffected. 321 advanceElapsedClock((long) (7.5 * MINUTE_IN_MILLIS)); 322 rescheduledJob = mService.getRescheduleJobForPeriodic(frequentJob); 323 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 324 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 325 326 // In last 1/6 of window. Next window start time should be shifted slightly. 327 advanceElapsedClock(6 * MINUTE_IN_MILLIS); 328 rescheduledJob = mService.getRescheduleJobForPeriodic(frequentJob); 329 assertEquals(nextWindowStartTime + MINUTE_IN_MILLIS, 330 rescheduledJob.getEarliestRunTime()); 331 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 332 333 JobStatus mediumJob = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow", 334 createJobInfo().setPeriodic(HOUR_IN_MILLIS)); 335 now = sElapsedRealtimeClock.millis(); 336 nextWindowStartTime = now + HOUR_IN_MILLIS; 337 nextWindowEndTime = now + 2 * HOUR_IN_MILLIS; 338 339 // At the beginning of the window. Next window should be unaffected. 340 rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob); 341 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 342 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 343 344 // Halfway through window. Next window should be unaffected. 345 advanceElapsedClock(30 * MINUTE_IN_MILLIS); 346 rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob); 347 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 348 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 349 350 // At the edge 1/6 of window. Next window should be unaffected. 351 advanceElapsedClock(20 * MINUTE_IN_MILLIS); 352 rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob); 353 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 354 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 355 356 // In last 1/6 of window. Next window start time should be shifted slightly. 357 advanceElapsedClock(6 * MINUTE_IN_MILLIS); 358 rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob); 359 assertEquals(nextWindowStartTime + (6 * MINUTE_IN_MILLIS), 360 rescheduledJob.getEarliestRunTime()); 361 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 362 363 JobStatus longJob = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow", 364 createJobInfo().setPeriodic(6 * HOUR_IN_MILLIS)); 365 now = sElapsedRealtimeClock.millis(); 366 nextWindowStartTime = now + 6 * HOUR_IN_MILLIS; 367 nextWindowEndTime = now + 12 * HOUR_IN_MILLIS; 368 369 // At the beginning of the window. Next window should be unaffected. 370 rescheduledJob = mService.getRescheduleJobForPeriodic(longJob); 371 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 372 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 373 374 // Halfway through window. Next window should be unaffected. 375 advanceElapsedClock(3 * HOUR_IN_MILLIS); 376 rescheduledJob = mService.getRescheduleJobForPeriodic(longJob); 377 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 378 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 379 380 // At the edge 1/6 of window. Next window should be unaffected. 381 advanceElapsedClock(2 * HOUR_IN_MILLIS); 382 rescheduledJob = mService.getRescheduleJobForPeriodic(longJob); 383 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 384 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 385 386 // In last 1/6 of window. Next window should be unaffected since we're over the shift cap. 387 advanceElapsedClock(15 * MINUTE_IN_MILLIS); 388 rescheduledJob = mService.getRescheduleJobForPeriodic(longJob); 389 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 390 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 391 392 // In last 1/6 of window. Next window start time should be shifted slightly. 393 advanceElapsedClock(30 * MINUTE_IN_MILLIS); 394 rescheduledJob = mService.getRescheduleJobForPeriodic(longJob); 395 assertEquals(nextWindowStartTime + (30 * MINUTE_IN_MILLIS), 396 rescheduledJob.getEarliestRunTime()); 397 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 398 399 // Flex duration close to period duration. 400 JobStatus gameyFlex = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow", 401 createJobInfo().setPeriodic(HOUR_IN_MILLIS, 59 * MINUTE_IN_MILLIS)); 402 now = sElapsedRealtimeClock.millis(); 403 nextWindowStartTime = now + HOUR_IN_MILLIS + MINUTE_IN_MILLIS; 404 nextWindowEndTime = now + 2 * HOUR_IN_MILLIS; 405 advanceElapsedClock(MINUTE_IN_MILLIS); 406 407 // At the beginning of the window. Next window should be unaffected. 408 rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex); 409 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 410 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 411 412 // Halfway through window. Next window should be unaffected. 413 advanceElapsedClock(29 * MINUTE_IN_MILLIS); 414 rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex); 415 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 416 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 417 418 // At the edge 1/6 of window. Next window should be unaffected. 419 advanceElapsedClock(20 * MINUTE_IN_MILLIS); 420 rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex); 421 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 422 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 423 424 // In last 1/6 of window. Next window start time should be shifted slightly. 425 advanceElapsedClock(6 * MINUTE_IN_MILLIS); 426 rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex); 427 assertEquals(nextWindowStartTime + (5 * MINUTE_IN_MILLIS), 428 rescheduledJob.getEarliestRunTime()); 429 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 430 431 // Very short flex duration compared to period duration. 432 JobStatus superFlex = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow", 433 createJobInfo().setPeriodic(HOUR_IN_MILLIS, 10 * MINUTE_IN_MILLIS)); 434 now = sElapsedRealtimeClock.millis(); 435 nextWindowStartTime = now + HOUR_IN_MILLIS + 50 * MINUTE_IN_MILLIS; 436 nextWindowEndTime = now + 2 * HOUR_IN_MILLIS; 437 advanceElapsedClock(MINUTE_IN_MILLIS); 438 439 // At the beginning of the window. Next window should be unaffected. 440 rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex); 441 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 442 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 443 444 // Halfway through window. Next window should be unaffected. 445 advanceElapsedClock(29 * MINUTE_IN_MILLIS); 446 rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex); 447 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 448 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 449 450 // At the edge 1/6 of window. Next window should be unaffected. 451 advanceElapsedClock(20 * MINUTE_IN_MILLIS); 452 rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex); 453 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 454 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 455 456 // In last 1/6 of window. Next window should be unaffected since the flex duration pushes 457 // the next window start time far enough away. 458 advanceElapsedClock(6 * MINUTE_IN_MILLIS); 459 rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex); 460 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 461 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 462 } 463 464 /** 465 * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job 466 * with the correct delay and deadline constraints if the periodic job with a custom flex 467 * setting is completed and rescheduled while run in its expected running window. 468 */ 469 @Test testGetRescheduleJobForPeriodic_insideWindow_flex()470 public void testGetRescheduleJobForPeriodic_insideWindow_flex() { 471 JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow_flex", 472 createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS)); 473 // First window starts 30 minutes from now. 474 advanceElapsedClock(30 * MINUTE_IN_MILLIS); 475 final long now = sElapsedRealtimeClock.millis(); 476 final long nextWindowStartTime = now + HOUR_IN_MILLIS; 477 final long nextWindowEndTime = now + 90 * MINUTE_IN_MILLIS; 478 479 JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job); 480 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 481 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 482 483 advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 10 minutes 484 485 rescheduledJob = mService.getRescheduleJobForPeriodic(job); 486 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 487 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 488 489 advanceElapsedClock(15 * MINUTE_IN_MILLIS); // now + 25 minutes 490 491 rescheduledJob = mService.getRescheduleJobForPeriodic(job); 492 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 493 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 494 495 advanceElapsedClock(4 * MINUTE_IN_MILLIS); // now + 29 minutes 496 497 rescheduledJob = mService.getRescheduleJobForPeriodic(job); 498 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 499 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 500 } 501 502 /** 503 * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job 504 * with the correct delay and deadline constraints if the periodic job failed but then ran 505 * successfully and was rescheduled while run in its expected running window. 506 */ 507 @Test testGetRescheduleJobForPeriodic_insideWindow_failedJob()508 public void testGetRescheduleJobForPeriodic_insideWindow_failedJob() { 509 final long now = sElapsedRealtimeClock.millis(); 510 final long nextWindowStartTime = now + HOUR_IN_MILLIS; 511 final long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS; 512 JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow_failedJob", 513 createJobInfo().setPeriodic(HOUR_IN_MILLIS)); 514 JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job); 515 516 JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob); 517 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 518 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 519 520 advanceElapsedClock(5 * MINUTE_IN_MILLIS); // now + 5 minutes 521 failedJob = mService.getRescheduleJobForFailureLocked(job); 522 advanceElapsedClock(5 * MINUTE_IN_MILLIS); // now + 10 minutes 523 524 rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob); 525 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 526 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 527 528 advanceElapsedClock(35 * MINUTE_IN_MILLIS); // now + 45 minutes 529 failedJob = mService.getRescheduleJobForFailureLocked(job); 530 advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 55 minutes 531 532 rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob); 533 // Shifted because it's close to the end of the window. 534 assertEquals(nextWindowStartTime + 5 * MINUTE_IN_MILLIS, 535 rescheduledJob.getEarliestRunTime()); 536 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 537 538 advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 57 minutes 539 failedJob = mService.getRescheduleJobForFailureLocked(job); 540 advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 59 minutes 541 542 rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob); 543 // Shifted because it's close to the end of the window. 544 assertEquals(nextWindowStartTime + 9 * MINUTE_IN_MILLIS, 545 rescheduledJob.getEarliestRunTime()); 546 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 547 } 548 549 /** 550 * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job 551 * with the correct delay and deadline constraints if the periodic job is completed and 552 * rescheduled when run after its expected running window. 553 */ 554 @Test testGetRescheduleJobForPeriodic_outsideWindow()555 public void testGetRescheduleJobForPeriodic_outsideWindow() { 556 JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow", 557 createJobInfo().setPeriodic(HOUR_IN_MILLIS)); 558 long now = sElapsedRealtimeClock.millis(); 559 long nextWindowStartTime = now + HOUR_IN_MILLIS; 560 long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS; 561 562 advanceElapsedClock(HOUR_IN_MILLIS + MINUTE_IN_MILLIS); 563 // Say the job ran at the very end of its previous window. The intended JSS behavior is to 564 // have consistent windows, so the new window should start as soon as the previous window 565 // ended and end PERIOD time after the previous window ended. 566 JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job); 567 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 568 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 569 570 advanceElapsedClock(2 * HOUR_IN_MILLIS); 571 // Say that the job ran at this point, possibly due to device idle. 572 // The next window should be consistent (start and end at the time it would have had the job 573 // run normally in previous windows). 574 nextWindowStartTime += 2 * HOUR_IN_MILLIS; 575 nextWindowEndTime += 2 * HOUR_IN_MILLIS; 576 577 rescheduledJob = mService.getRescheduleJobForPeriodic(job); 578 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 579 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 580 } 581 582 /** 583 * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job 584 * with the correct delay and deadline constraints if the periodic job with a custom flex 585 * setting is completed and rescheduled when run after its expected running window. 586 */ 587 @Test testGetRescheduleJobForPeriodic_outsideWindow_flex()588 public void testGetRescheduleJobForPeriodic_outsideWindow_flex() { 589 JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow_flex", 590 createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS)); 591 // First window starts 30 minutes from now. 592 advanceElapsedClock(30 * MINUTE_IN_MILLIS); 593 long now = sElapsedRealtimeClock.millis(); 594 long nextWindowStartTime = now + HOUR_IN_MILLIS; 595 long nextWindowEndTime = now + 90 * MINUTE_IN_MILLIS; 596 597 advanceElapsedClock(31 * MINUTE_IN_MILLIS); 598 // Say the job ran at the very end of its previous window. The intended JSS behavior is to 599 // have consistent windows, so the new window should start as soon as the previous window 600 // ended and end PERIOD time after the previous window ended. 601 JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job); 602 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 603 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 604 605 // 5 minutes before the start of the next window. It's too close to the next window, so the 606 // returned job should be for the window after. 607 advanceElapsedClock(24 * MINUTE_IN_MILLIS); 608 nextWindowStartTime += HOUR_IN_MILLIS; 609 nextWindowEndTime += HOUR_IN_MILLIS; 610 rescheduledJob = mService.getRescheduleJobForPeriodic(job); 611 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 612 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 613 614 advanceElapsedClock(2 * HOUR_IN_MILLIS + 10 * MINUTE_IN_MILLIS); 615 // Say that the job ran at this point, possibly due to device idle. 616 // The next window should be consistent (start and end at the time it would have had the job 617 // run normally in previous windows). 618 nextWindowStartTime += 2 * HOUR_IN_MILLIS; 619 nextWindowEndTime += 2 * HOUR_IN_MILLIS; 620 621 rescheduledJob = mService.getRescheduleJobForPeriodic(job); 622 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 623 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 624 } 625 626 /** 627 * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job 628 * with the correct delay and deadline constraints if the periodic job failed but then ran 629 * successfully and was rescheduled when run after its expected running window. 630 */ 631 @Test testGetRescheduleJobForPeriodic_outsideWindow_failedJob()632 public void testGetRescheduleJobForPeriodic_outsideWindow_failedJob() { 633 JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow_failedJob", 634 createJobInfo().setPeriodic(HOUR_IN_MILLIS)); 635 JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job); 636 long now = sElapsedRealtimeClock.millis(); 637 long nextWindowStartTime = now + HOUR_IN_MILLIS; 638 long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS; 639 640 advanceElapsedClock(HOUR_IN_MILLIS + MINUTE_IN_MILLIS); 641 // Say the job ran at the very end of its previous window. The intended JSS behavior is to 642 // have consistent windows, so the new window should start as soon as the previous window 643 // ended and end PERIOD time after the previous window ended. 644 JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob); 645 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 646 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 647 648 advanceElapsedClock(2 * HOUR_IN_MILLIS); 649 // Say that the job ran at this point, possibly due to device idle. 650 // The next window should be consistent (start and end at the time it would have had the job 651 // run normally in previous windows). 652 nextWindowStartTime += 2 * HOUR_IN_MILLIS; 653 nextWindowEndTime += 2 * HOUR_IN_MILLIS; 654 655 rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob); 656 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 657 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 658 } 659 660 /** 661 * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job 662 * with the correct delay and deadline constraints if the periodic job with a custom flex 663 * setting failed but then ran successfully and was rescheduled when run after its expected 664 * running window. 665 */ 666 @Test testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob()667 public void testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob() { 668 JobStatus job = createJobStatus( 669 "testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob", 670 createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS)); 671 JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job); 672 // First window starts 30 minutes from now. 673 advanceElapsedClock(30 * MINUTE_IN_MILLIS); 674 long now = sElapsedRealtimeClock.millis(); 675 long nextWindowStartTime = now + HOUR_IN_MILLIS; 676 long nextWindowEndTime = now + 90 * MINUTE_IN_MILLIS; 677 678 advanceElapsedClock(31 * MINUTE_IN_MILLIS); 679 // Say the job ran at the very end of its previous window. The intended JSS behavior is to 680 // have consistent windows, so the new window should start as soon as the previous window 681 // ended and end PERIOD time after the previous window ended. 682 JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob); 683 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 684 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 685 686 // 5 minutes before the start of the next window. It's too close to the next window, so the 687 // returned job should be for the window after. 688 advanceElapsedClock(24 * MINUTE_IN_MILLIS); 689 nextWindowStartTime += HOUR_IN_MILLIS; 690 nextWindowEndTime += HOUR_IN_MILLIS; 691 rescheduledJob = mService.getRescheduleJobForPeriodic(job); 692 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 693 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 694 695 advanceElapsedClock(2 * HOUR_IN_MILLIS); 696 // Say that the job ran at this point, possibly due to device idle. 697 // The next window should be consistent (start and end at the time it would have had the job 698 // run normally in previous windows). 699 nextWindowStartTime += 2 * HOUR_IN_MILLIS; 700 nextWindowEndTime += 2 * HOUR_IN_MILLIS; 701 702 rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob); 703 assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); 704 assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); 705 } 706 707 /** Tests that rare job batching works as expected. */ 708 @Test testRareJobBatching()709 public void testRareJobBatching() { 710 spyOn(mService); 711 doNothing().when(mService).evaluateControllerStatesLocked(any()); 712 doNothing().when(mService).noteJobsPending(any()); 713 doReturn(true).when(mService).isReadyToBeExecutedLocked(any()); 714 advanceElapsedClock(24 * HOUR_IN_MILLIS); 715 716 JobSchedulerService.MaybeReadyJobQueueFunctor maybeQueueFunctor = 717 mService.new MaybeReadyJobQueueFunctor(); 718 mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT = 5; 719 mService.mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = HOUR_IN_MILLIS; 720 721 JobStatus job = createJobStatus( 722 "testRareJobBatching", 723 createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); 724 job.setStandbyBucket(RARE_INDEX); 725 726 // Not enough RARE jobs to run. 727 mService.mPendingJobs.clear(); 728 maybeQueueFunctor.reset(); 729 for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) { 730 maybeQueueFunctor.accept(job); 731 assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount); 732 assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size()); 733 assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed()); 734 } 735 maybeQueueFunctor.postProcessLocked(); 736 assertEquals(0, mService.mPendingJobs.size()); 737 738 // Enough RARE jobs to run. 739 mService.mPendingJobs.clear(); 740 maybeQueueFunctor.reset(); 741 for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT; ++i) { 742 maybeQueueFunctor.accept(job); 743 assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount); 744 assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size()); 745 assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed()); 746 } 747 maybeQueueFunctor.postProcessLocked(); 748 assertEquals(5, mService.mPendingJobs.size()); 749 750 // Not enough RARE jobs to run, but a non-batched job saves the day. 751 mService.mPendingJobs.clear(); 752 maybeQueueFunctor.reset(); 753 JobStatus activeJob = createJobStatus( 754 "testRareJobBatching", 755 createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); 756 activeJob.setStandbyBucket(ACTIVE_INDEX); 757 for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) { 758 maybeQueueFunctor.accept(job); 759 assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount); 760 assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size()); 761 assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed()); 762 } 763 maybeQueueFunctor.accept(activeJob); 764 maybeQueueFunctor.postProcessLocked(); 765 assertEquals(3, mService.mPendingJobs.size()); 766 767 // Not enough RARE jobs to run, but an old RARE job saves the day. 768 mService.mPendingJobs.clear(); 769 maybeQueueFunctor.reset(); 770 JobStatus oldRareJob = createJobStatus("testRareJobBatching", createJobInfo()); 771 oldRareJob.setStandbyBucket(RARE_INDEX); 772 final long oldBatchTime = sElapsedRealtimeClock.millis() 773 - 2 * mService.mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS; 774 oldRareJob.setFirstForceBatchedTimeElapsed(oldBatchTime); 775 for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) { 776 maybeQueueFunctor.accept(job); 777 assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount); 778 assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size()); 779 assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed()); 780 } 781 maybeQueueFunctor.accept(oldRareJob); 782 assertEquals(oldBatchTime, oldRareJob.getFirstForceBatchedTimeElapsed()); 783 maybeQueueFunctor.postProcessLocked(); 784 assertEquals(3, mService.mPendingJobs.size()); 785 } 786 787 /** Tests that jobs scheduled by the app itself are counted towards scheduling limits. */ 788 @Test testScheduleLimiting_RegularSchedule_Blocked()789 public void testScheduleLimiting_RegularSchedule_Blocked() { 790 mService.mConstants.ENABLE_API_QUOTAS = true; 791 mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300; 792 mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000; 793 mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; 794 mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; 795 mService.updateQuotaTracker(); 796 797 final JobInfo job = createJobInfo().setPersisted(true).build(); 798 for (int i = 0; i < 500; ++i) { 799 final int expected = 800 i < 300 ? JobScheduler.RESULT_SUCCESS : JobScheduler.RESULT_FAILURE; 801 assertEquals("Got unexpected result for schedule #" + (i + 1), 802 expected, 803 mService.scheduleAsPackage(job, null, 10123, null, 0, "")); 804 } 805 } 806 807 /** 808 * Tests that jobs scheduled by the app itself succeed even if the app is above the scheduling 809 * limit. 810 */ 811 @Test 812 public void testScheduleLimiting_RegularSchedule_Allowed() { 813 mService.mConstants.ENABLE_API_QUOTAS = true; 814 mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300; 815 mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000; 816 mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; 817 mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; 818 mService.updateQuotaTracker(); 819 820 final JobInfo job = createJobInfo().setPersisted(true).build(); 821 for (int i = 0; i < 500; ++i) { 822 assertEquals("Got unexpected result for schedule #" + (i + 1), 823 JobScheduler.RESULT_SUCCESS, 824 mService.scheduleAsPackage(job, null, 10123, null, 0, "")); 825 } 826 } 827 828 /** 829 * Tests that jobs scheduled through a proxy (eg. system server) don't count towards scheduling 830 * limits. 831 */ 832 @Test 833 public void testScheduleLimiting_Proxy() { 834 mService.mConstants.ENABLE_API_QUOTAS = true; 835 mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300; 836 mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000; 837 mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; 838 mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; 839 mService.updateQuotaTracker(); 840 841 final JobInfo job = createJobInfo().setPersisted(true).build(); 842 for (int i = 0; i < 500; ++i) { 843 assertEquals("Got unexpected result for schedule #" + (i + 1), 844 JobScheduler.RESULT_SUCCESS, 845 mService.scheduleAsPackage(job, null, 10123, "proxied.package", 0, "")); 846 } 847 } 848 849 /** 850 * Tests that jobs scheduled by an app for itself as if through a proxy are counted towards 851 * scheduling limits. 852 */ 853 @Test 854 public void testScheduleLimiting_SelfProxy() { 855 mService.mConstants.ENABLE_API_QUOTAS = true; 856 mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300; 857 mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000; 858 mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; 859 mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; 860 mService.updateQuotaTracker(); 861 862 final JobInfo job = createJobInfo().setPersisted(true).build(); 863 for (int i = 0; i < 500; ++i) { 864 final int expected = 865 i < 300 ? JobScheduler.RESULT_SUCCESS : JobScheduler.RESULT_FAILURE; 866 assertEquals("Got unexpected result for schedule #" + (i + 1), 867 expected, 868 mService.scheduleAsPackage(job, null, 10123, job.getService().getPackageName(), 869 0, "")); 870 } 871 } 872 873 @Test 874 public void testPendingJobSorting() { 875 // First letter in job variable name indicate regular (r) or expedited (e). 876 // Capital letters in job variable name indicate the app/UID. 877 // Numbers in job variable name indicate the enqueue time. 878 // Expected sort order: 879 // eA7 > rA1 > eB6 > rB2 > eC3 > rD4 > eE5 > eF9 > rF8 > eC11 > rC10 > rG12 > rG13 > eE14 880 // Intentions: 881 // * A jobs let us test skipping both regular and expedited jobs of other apps 882 // * B jobs let us test skipping only regular job of another app without going too far 883 // * C jobs test that regular jobs don't skip over other app's jobs and that EJs only 884 // skip up to level of the earliest regular job 885 // * E jobs test that expedited jobs don't skip the line when the app has no regular jobs 886 // * F jobs test correct expedited/regular ordering doesn't push jobs too high in list 887 // * G jobs test correct ordering for regular jobs 888 // * H job tests correct behavior when enqueue times are the same 889 JobStatus rA1 = createJobStatus("testPendingJobSorting", createJobInfo(1), 1); 890 JobStatus rB2 = createJobStatus("testPendingJobSorting", createJobInfo(2), 2); 891 JobStatus eC3 = createJobStatus("testPendingJobSorting", 892 createJobInfo(3).setExpedited(true), 3); 893 JobStatus rD4 = createJobStatus("testPendingJobSorting", createJobInfo(4), 4); 894 JobStatus eE5 = createJobStatus("testPendingJobSorting", 895 createJobInfo(5).setExpedited(true), 5); 896 JobStatus eB6 = createJobStatus("testPendingJobSorting", 897 createJobInfo(6).setExpedited(true), 2); 898 JobStatus eA7 = createJobStatus("testPendingJobSorting", 899 createJobInfo(7).setExpedited(true), 1); 900 JobStatus rH8 = createJobStatus("testPendingJobSorting", createJobInfo(8), 14); 901 JobStatus rF8 = createJobStatus("testPendingJobSorting", createJobInfo(8), 6); 902 JobStatus eF9 = createJobStatus("testPendingJobSorting", 903 createJobInfo(9).setExpedited(true), 6); 904 JobStatus rC10 = createJobStatus("testPendingJobSorting", createJobInfo(10), 3); 905 JobStatus eC11 = createJobStatus("testPendingJobSorting", 906 createJobInfo(11).setExpedited(true), 3); 907 JobStatus rG12 = createJobStatus("testPendingJobSorting", createJobInfo(12), 7); 908 JobStatus rG13 = createJobStatus("testPendingJobSorting", createJobInfo(13), 7); 909 JobStatus eE14 = createJobStatus("testPendingJobSorting", 910 createJobInfo(14).setExpedited(true), 5); 911 912 rA1.enqueueTime = 1; 913 rB2.enqueueTime = 2; 914 eC3.enqueueTime = 3; 915 rD4.enqueueTime = 4; 916 eE5.enqueueTime = 5; 917 eB6.enqueueTime = 6; 918 eA7.enqueueTime = 7; 919 rF8.enqueueTime = 8; 920 rH8.enqueueTime = 8; 921 eF9.enqueueTime = 9; 922 rC10.enqueueTime = 10; 923 eC11.enqueueTime = 11; 924 rG12.enqueueTime = 12; 925 rG13.enqueueTime = 13; 926 eE14.enqueueTime = 14; 927 928 mService.mPendingJobs.clear(); 929 // Add in random order so sorting is apparent. 930 mService.mPendingJobs.add(eC3); 931 mService.mPendingJobs.add(eE5); 932 mService.mPendingJobs.add(rA1); 933 mService.mPendingJobs.add(rG13); 934 mService.mPendingJobs.add(rD4); 935 mService.mPendingJobs.add(eA7); 936 mService.mPendingJobs.add(rG12); 937 mService.mPendingJobs.add(rH8); 938 mService.mPendingJobs.add(rF8); 939 mService.mPendingJobs.add(eB6); 940 mService.mPendingJobs.add(eE14); 941 mService.mPendingJobs.add(eF9); 942 mService.mPendingJobs.add(rB2); 943 mService.mPendingJobs.add(rC10); 944 mService.mPendingJobs.add(eC11); 945 946 mService.mPendingJobComparator.refreshLocked(); 947 mService.mPendingJobs.sort(mService.mPendingJobComparator); 948 949 final JobStatus[] expectedOrder = new JobStatus[]{ 950 eA7, rA1, eB6, rB2, eC3, rD4, eE5, eF9, rH8, rF8, eC11, rC10, rG12, rG13, eE14}; 951 for (int i = 0; i < expectedOrder.length; ++i) { 952 assertEquals("List wasn't correctly sorted @ index " + i, 953 expectedOrder[i].getJobId(), mService.mPendingJobs.get(i).getJobId()); 954 } 955 } 956 957 private void checkPendingJobInvariants() { 958 long regJobEnqueueTime = 0; 959 final SparseBooleanArray regJobSeen = new SparseBooleanArray(); 960 final SparseLongArray ejEnqueueTimes = new SparseLongArray(); 961 962 for (int i = 0; i < mService.mPendingJobs.size(); ++i) { 963 final JobStatus job = mService.mPendingJobs.get(i); 964 final int uid = job.getSourceUid(); 965 966 if (!job.isRequestedExpeditedJob()) { 967 // Invariant #1: Regular jobs are sorted by enqueue time. 968 assertTrue("Regular job with earlier enqueue time sorted after a later time: " 969 + regJobEnqueueTime + " vs " + job.enqueueTime, 970 regJobEnqueueTime <= job.enqueueTime); 971 regJobEnqueueTime = job.enqueueTime; 972 regJobSeen.put(uid, true); 973 } else { 974 // Invariant #2: EJs should be before regular jobs for an individual app 975 if (regJobSeen.get(uid)) { 976 fail("UID " + uid + " had an EJ ordered after a regular job"); 977 } 978 final long ejEnqueueTime = ejEnqueueTimes.get(uid, 0); 979 // Invariant #3: EJs for an individual app should be sorted by enqueue time. 980 assertTrue("EJ with earlier enqueue time sorted after a later time: " 981 + ejEnqueueTime + " vs " + job.enqueueTime, 982 ejEnqueueTime <= job.enqueueTime); 983 ejEnqueueTimes.put(uid, job.enqueueTime); 984 } 985 } 986 } 987 988 private static String sortedJobToString(JobStatus job) { 989 return "testJob " + job.getSourceUid() + "/" + job.getJobId() + "/" 990 + job.isRequestedExpeditedJob() + "@" + job.enqueueTime; 991 } 992 993 @Test 994 public void testPendingJobSorting_Random() { 995 Random random = new Random(1); // Always use the same series of pseudo random values. 996 997 mService.mPendingJobs.clear(); 998 999 for (int i = 0; i < 2500; ++i) { 1000 JobStatus job = createJobStatus("testPendingJobSorting_Random", 1001 createJobInfo(i).setExpedited(random.nextBoolean()), random.nextInt(250)); 1002 job.enqueueTime = random.nextInt(1_000_000); 1003 mService.mPendingJobs.add(job); 1004 1005 mService.mPendingJobComparator.refreshLocked(); 1006 try { 1007 mService.mPendingJobs.sort(mService.mPendingJobComparator); 1008 } catch (Exception e) { 1009 for (JobStatus toDump : mService.mPendingJobs) { 1010 Log.i(TAG, sortedJobToString(toDump)); 1011 } 1012 throw e; 1013 } 1014 checkPendingJobInvariants(); 1015 } 1016 } 1017 1018 private int sign(int i) { 1019 if (i > 0) { 1020 return 1; 1021 } 1022 if (i < 0) { 1023 return -1; 1024 } 1025 return 0; 1026 } 1027 1028 @Test 1029 public void testPendingJobSortingTransitivity() { 1030 // Always use the same series of pseudo random values. 1031 for (int seed : new int[]{1337, 7357, 606, 6357, 41106010, 3, 2, 1}) { 1032 Random random = new Random(seed); 1033 1034 mService.mPendingJobs.clear(); 1035 1036 for (int i = 0; i < 300; ++i) { 1037 JobStatus job = createJobStatus("testPendingJobSortingTransitivity", 1038 createJobInfo(i).setExpedited(random.nextBoolean()), random.nextInt(50)); 1039 job.enqueueTime = random.nextInt(1_000_000); 1040 job.overrideState = random.nextInt(4); 1041 mService.mPendingJobs.add(job); 1042 } 1043 1044 verifyPendingJobComparatorTransitivity(); 1045 } 1046 } 1047 1048 @Test 1049 public void testPendingJobSortingTransitivity_Concentrated() { 1050 // Always use the same series of pseudo random values. 1051 for (int seed : new int[]{1337, 6000, 637739, 6357, 1, 7, 13}) { 1052 Random random = new Random(seed); 1053 1054 mService.mPendingJobs.clear(); 1055 1056 for (int i = 0; i < 300; ++i) { 1057 JobStatus job = createJobStatus("testPendingJobSortingTransitivity_Concentrated", 1058 createJobInfo(i).setExpedited(random.nextFloat() < .03), 1059 random.nextInt(20)); 1060 job.enqueueTime = random.nextInt(250); 1061 job.overrideState = random.nextFloat() < .01 1062 ? JobStatus.OVERRIDE_SORTING : JobStatus.OVERRIDE_NONE; 1063 mService.mPendingJobs.add(job); 1064 Log.d(TAG, sortedJobToString(job)); 1065 } 1066 1067 verifyPendingJobComparatorTransitivity(); 1068 } 1069 } 1070 1071 private void verifyPendingJobComparatorTransitivity() { 1072 mService.mPendingJobComparator.refreshLocked(); 1073 1074 for (int i = 0; i < mService.mPendingJobs.size(); ++i) { 1075 final JobStatus job1 = mService.mPendingJobs.get(i); 1076 1077 for (int j = 0; j < mService.mPendingJobs.size(); ++j) { 1078 final JobStatus job2 = mService.mPendingJobs.get(j); 1079 final int sign12 = sign(mService.mPendingJobComparator.compare(job1, job2)); 1080 final int sign21 = sign(mService.mPendingJobComparator.compare(job2, job1)); 1081 if (sign12 != -sign21) { 1082 final String job1String = sortedJobToString(job1); 1083 final String job2String = sortedJobToString(job2); 1084 fail("compare(" + job1String + ", " + job2String + ") != " 1085 + "-compare(" + job2String + ", " + job1String + ")"); 1086 } 1087 1088 for (int k = 0; k < mService.mPendingJobs.size(); ++k) { 1089 final JobStatus job3 = mService.mPendingJobs.get(k); 1090 final int sign23 = sign(mService.mPendingJobComparator.compare(job2, job3)); 1091 final int sign13 = sign(mService.mPendingJobComparator.compare(job1, job3)); 1092 1093 // Confirm 1 < 2 < 3 or 1 > 2 > 3 or 1 == 2 == 3 1094 if ((sign12 == sign23 && sign12 != sign13) 1095 // Confirm that if 1 == 2, then (1 < 3 AND 2 < 3) OR (1 > 3 && 2 > 3) 1096 || (sign12 == 0 && sign13 != sign23)) { 1097 final String job1String = sortedJobToString(job1); 1098 final String job2String = sortedJobToString(job2); 1099 final String job3String = sortedJobToString(job3); 1100 fail("Transitivity fail" 1101 + ": compare(" + job1String + ", " + job2String + ")=" + sign12 1102 + ", compare(" + job2String + ", " + job3String + ")=" + sign23 1103 + ", compare(" + job1String + ", " + job3String + ")=" + sign13); 1104 } 1105 } 1106 } 1107 } 1108 } 1109 } 1110