blob: 00495f3ec02d48a258381b2abcb18a3ff4b5c9df [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.job;
import static android.text.format.DateUtils.DAY_IN_MILLIS;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
import static com.android.server.job.JobSchedulerService.RARE_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.IActivityManager;
import android.app.job.JobInfo;
import android.app.usage.UsageStatsManagerInternal;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.NetworkPolicyManager;
import android.os.BatteryManagerInternal;
import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import com.android.server.AppStateTracker;
import com.android.server.DeviceIdleInternal;
import com.android.server.LocalServices;
import com.android.server.job.controllers.JobStatus;
import com.android.server.usage.AppStandbyInternal;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
import java.time.Clock;
import java.time.Duration;
import java.time.ZoneOffset;
public class JobSchedulerServiceTest {
private JobSchedulerService mService;
private MockitoSession mMockingSession;
@Mock
private ActivityManagerInternal mActivityMangerInternal;
@Mock
private Context mContext;
private class TestJobSchedulerService extends JobSchedulerService {
TestJobSchedulerService(Context context) {
super(context);
}
@Override
public boolean isChainedAttributionEnabled() {
return false;
}
}
@Before
public void setUp() {
mMockingSession = mockitoSession()
.initMocks(this)
.strictness(Strictness.LENIENT)
.mockStatic(LocalServices.class)
.mockStatic(ServiceManager.class)
.startMocking();
// Called in JobSchedulerService constructor.
when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
doReturn(mActivityMangerInternal)
.when(() -> LocalServices.getService(ActivityManagerInternal.class));
doReturn(mock(AppStandbyInternal.class))
.when(() -> LocalServices.getService(AppStandbyInternal.class));
doReturn(mock(UsageStatsManagerInternal.class))
.when(() -> LocalServices.getService(UsageStatsManagerInternal.class));
// Called in BackgroundJobsController constructor.
doReturn(mock(AppStateTracker.class))
.when(() -> LocalServices.getService(AppStateTracker.class));
// Called in BatteryController constructor.
doReturn(mock(BatteryManagerInternal.class))
.when(() -> LocalServices.getService(BatteryManagerInternal.class));
// Called in ConnectivityController constructor.
when(mContext.getSystemService(ConnectivityManager.class))
.thenReturn(mock(ConnectivityManager.class));
when(mContext.getSystemService(NetworkPolicyManager.class))
.thenReturn(mock(NetworkPolicyManager.class));
// Called in DeviceIdleJobsController constructor.
doReturn(mock(DeviceIdleInternal.class))
.when(() -> LocalServices.getService(DeviceIdleInternal.class));
// Used in JobStatus.
doReturn(mock(PackageManagerInternal.class))
.when(() -> LocalServices.getService(PackageManagerInternal.class));
// Called via IdleController constructor.
when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
when(mContext.getResources()).thenReturn(mock(Resources.class));
// Called in QuotaController constructor.
IActivityManager activityManager = ActivityManager.getService();
spyOn(activityManager);
try {
doNothing().when(activityManager).registerUidObserver(any(), anyInt(), anyInt(), any());
} catch (RemoteException e) {
fail("registerUidObserver threw exception: " + e.getMessage());
}
JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
mService = new TestJobSchedulerService(mContext);
}
@After
public void tearDown() {
if (mMockingSession != null) {
mMockingSession.finishMocking();
}
}
private Clock getAdvancedClock(Clock clock, long incrementMs) {
return Clock.offset(clock, Duration.ofMillis(incrementMs));
}
private void advanceElapsedClock(long incrementMs) {
JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock(
JobSchedulerService.sElapsedRealtimeClock, incrementMs);
}
private static JobInfo.Builder createJobInfo() {
return new JobInfo.Builder(351, new ComponentName("foo", "bar"));
}
private JobStatus createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder) {
return JobStatus.createFromJobInfo(
jobInfoBuilder.build(), 1234, "com.android.test", 0, testTag);
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job is scheduled with the
* minimum possible period.
*/
@Test
public void testGetRescheduleJobForPeriodic_minPeriod() {
final long now = sElapsedRealtimeClock.millis();
JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow",
createJobInfo().setPeriodic(15 * MINUTE_IN_MILLIS));
final long nextWindowStartTime = now + 15 * MINUTE_IN_MILLIS;
final long nextWindowEndTime = now + 30 * MINUTE_IN_MILLIS;
for (int i = 0; i < 25; i++) {
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(30_000); // 30 seconds
}
for (int i = 0; i < 5; i++) {
// Window buffering in last 1/6 of window.
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime + i * 30_000, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(30_000); // 30 seconds
}
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job is scheduled with a
* period that's too large.
*/
@Test
public void testGetRescheduleJobForPeriodic_largePeriod() {
final long now = sElapsedRealtimeClock.millis();
JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow",
createJobInfo().setPeriodic(2 * 365 * DAY_IN_MILLIS));
assertEquals(now, job.getEarliestRunTime());
// Periods are capped at 365 days (1 year).
assertEquals(now + 365 * DAY_IN_MILLIS, job.getLatestRunTimeElapsed());
final long nextWindowStartTime = now + 365 * DAY_IN_MILLIS;
final long nextWindowEndTime = nextWindowStartTime + 365 * DAY_IN_MILLIS;
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job is completed and
* rescheduled while run in its expected running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_insideWindow() {
final long now = sElapsedRealtimeClock.millis();
JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow",
createJobInfo().setPeriodic(HOUR_IN_MILLIS));
final long nextWindowStartTime = now + HOUR_IN_MILLIS;
final long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 10 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(20 * MINUTE_IN_MILLIS); // now + 30 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(25 * MINUTE_IN_MILLIS); // now + 55 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
// Shifted because it's close to the end of the window.
assertEquals(nextWindowStartTime + 5 * MINUTE_IN_MILLIS,
rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(4 * MINUTE_IN_MILLIS); // now + 59 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
// Shifted because it's close to the end of the window.
assertEquals(nextWindowStartTime + 9 * MINUTE_IN_MILLIS,
rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with an extra delay and correct deadline constraint if the periodic job is completed near the
* end of its expected running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_closeToEndOfWindow() {
JobStatus frequentJob = createJobStatus(
"testGetRescheduleJobForPeriodic_closeToEndOfWindow",
createJobInfo().setPeriodic(15 * MINUTE_IN_MILLIS));
long now = sElapsedRealtimeClock.millis();
long nextWindowStartTime = now + 15 * MINUTE_IN_MILLIS;
long nextWindowEndTime = now + 30 * MINUTE_IN_MILLIS;
// At the beginning of the window. Next window should be unaffected.
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(frequentJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// Halfway through window. Next window should be unaffected.
advanceElapsedClock((long) (7.5 * MINUTE_IN_MILLIS));
rescheduledJob = mService.getRescheduleJobForPeriodic(frequentJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// In last 1/6 of window. Next window start time should be shifted slightly.
advanceElapsedClock(6 * MINUTE_IN_MILLIS);
rescheduledJob = mService.getRescheduleJobForPeriodic(frequentJob);
assertEquals(nextWindowStartTime + MINUTE_IN_MILLIS,
rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
JobStatus mediumJob = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow",
createJobInfo().setPeriodic(HOUR_IN_MILLIS));
now = sElapsedRealtimeClock.millis();
nextWindowStartTime = now + HOUR_IN_MILLIS;
nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
// At the beginning of the window. Next window should be unaffected.
rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// Halfway through window. Next window should be unaffected.
advanceElapsedClock(30 * MINUTE_IN_MILLIS);
rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// At the edge 1/6 of window. Next window should be unaffected.
advanceElapsedClock(20 * MINUTE_IN_MILLIS);
rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// In last 1/6 of window. Next window start time should be shifted slightly.
advanceElapsedClock(6 * MINUTE_IN_MILLIS);
rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob);
assertEquals(nextWindowStartTime + (6 * MINUTE_IN_MILLIS),
rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
JobStatus longJob = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow",
createJobInfo().setPeriodic(6 * HOUR_IN_MILLIS));
now = sElapsedRealtimeClock.millis();
nextWindowStartTime = now + 6 * HOUR_IN_MILLIS;
nextWindowEndTime = now + 12 * HOUR_IN_MILLIS;
// At the beginning of the window. Next window should be unaffected.
rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// Halfway through window. Next window should be unaffected.
advanceElapsedClock(3 * HOUR_IN_MILLIS);
rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// At the edge 1/6 of window. Next window should be unaffected.
advanceElapsedClock(2 * HOUR_IN_MILLIS);
rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// In last 1/6 of window. Next window should be unaffected since we're over the shift cap.
advanceElapsedClock(15 * MINUTE_IN_MILLIS);
rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// In last 1/6 of window. Next window start time should be shifted slightly.
advanceElapsedClock(30 * MINUTE_IN_MILLIS);
rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
assertEquals(nextWindowStartTime + (30 * MINUTE_IN_MILLIS),
rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// Flex duration close to period duration.
JobStatus gameyFlex = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow",
createJobInfo().setPeriodic(HOUR_IN_MILLIS, 59 * MINUTE_IN_MILLIS));
now = sElapsedRealtimeClock.millis();
nextWindowStartTime = now + HOUR_IN_MILLIS + MINUTE_IN_MILLIS;
nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
advanceElapsedClock(MINUTE_IN_MILLIS);
// At the beginning of the window. Next window should be unaffected.
rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// Halfway through window. Next window should be unaffected.
advanceElapsedClock(29 * MINUTE_IN_MILLIS);
rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// At the edge 1/6 of window. Next window should be unaffected.
advanceElapsedClock(20 * MINUTE_IN_MILLIS);
rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// In last 1/6 of window. Next window start time should be shifted slightly.
advanceElapsedClock(6 * MINUTE_IN_MILLIS);
rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex);
assertEquals(nextWindowStartTime + (5 * MINUTE_IN_MILLIS),
rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// Very short flex duration compared to period duration.
JobStatus superFlex = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow",
createJobInfo().setPeriodic(HOUR_IN_MILLIS, 10 * MINUTE_IN_MILLIS));
now = sElapsedRealtimeClock.millis();
nextWindowStartTime = now + HOUR_IN_MILLIS + 50 * MINUTE_IN_MILLIS;
nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
advanceElapsedClock(MINUTE_IN_MILLIS);
// At the beginning of the window. Next window should be unaffected.
rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// Halfway through window. Next window should be unaffected.
advanceElapsedClock(29 * MINUTE_IN_MILLIS);
rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// At the edge 1/6 of window. Next window should be unaffected.
advanceElapsedClock(20 * MINUTE_IN_MILLIS);
rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// In last 1/6 of window. Next window should be unaffected since the flex duration pushes
// the next window start time far enough away.
advanceElapsedClock(6 * MINUTE_IN_MILLIS);
rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job with a custom flex
* setting is completed and rescheduled while run in its expected running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_insideWindow_flex() {
JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow_flex",
createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS));
// First window starts 30 minutes from now.
advanceElapsedClock(30 * MINUTE_IN_MILLIS);
final long now = sElapsedRealtimeClock.millis();
final long nextWindowStartTime = now + HOUR_IN_MILLIS;
final long nextWindowEndTime = now + 90 * MINUTE_IN_MILLIS;
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 10 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(15 * MINUTE_IN_MILLIS); // now + 25 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(4 * MINUTE_IN_MILLIS); // now + 29 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job failed but then ran
* successfully and was rescheduled while run in its expected running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_insideWindow_failedJob() {
final long now = sElapsedRealtimeClock.millis();
final long nextWindowStartTime = now + HOUR_IN_MILLIS;
final long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow_failedJob",
createJobInfo().setPeriodic(HOUR_IN_MILLIS));
JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job);
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(5 * MINUTE_IN_MILLIS); // now + 5 minutes
failedJob = mService.getRescheduleJobForFailureLocked(job);
advanceElapsedClock(5 * MINUTE_IN_MILLIS); // now + 10 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(35 * MINUTE_IN_MILLIS); // now + 45 minutes
failedJob = mService.getRescheduleJobForFailureLocked(job);
advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 55 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
// Shifted because it's close to the end of the window.
assertEquals(nextWindowStartTime + 5 * MINUTE_IN_MILLIS,
rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 57 minutes
failedJob = mService.getRescheduleJobForFailureLocked(job);
advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 59 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
// Shifted because it's close to the end of the window.
assertEquals(nextWindowStartTime + 9 * MINUTE_IN_MILLIS,
rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job is completed and
* rescheduled when run after its expected running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_outsideWindow() {
JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow",
createJobInfo().setPeriodic(HOUR_IN_MILLIS));
long now = sElapsedRealtimeClock.millis();
long nextWindowStartTime = now + HOUR_IN_MILLIS;
long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
advanceElapsedClock(HOUR_IN_MILLIS + MINUTE_IN_MILLIS);
// Say the job ran at the very end of its previous window. The intended JSS behavior is to
// have consistent windows, so the new window should start as soon as the previous window
// ended and end PERIOD time after the previous window ended.
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(2 * HOUR_IN_MILLIS);
// Say that the job ran at this point, possibly due to device idle.
// The next window should be consistent (start and end at the time it would have had the job
// run normally in previous windows).
nextWindowStartTime += 2 * HOUR_IN_MILLIS;
nextWindowEndTime += 2 * HOUR_IN_MILLIS;
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job with a custom flex
* setting is completed and rescheduled when run after its expected running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_outsideWindow_flex() {
JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow_flex",
createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS));
// First window starts 30 minutes from now.
advanceElapsedClock(30 * MINUTE_IN_MILLIS);
long now = sElapsedRealtimeClock.millis();
long nextWindowStartTime = now + HOUR_IN_MILLIS;
long nextWindowEndTime = now + 90 * MINUTE_IN_MILLIS;
advanceElapsedClock(31 * MINUTE_IN_MILLIS);
// Say the job ran at the very end of its previous window. The intended JSS behavior is to
// have consistent windows, so the new window should start as soon as the previous window
// ended and end PERIOD time after the previous window ended.
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// 5 minutes before the start of the next window. It's too close to the next window, so the
// returned job should be for the window after.
advanceElapsedClock(24 * MINUTE_IN_MILLIS);
nextWindowStartTime += HOUR_IN_MILLIS;
nextWindowEndTime += HOUR_IN_MILLIS;
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(2 * HOUR_IN_MILLIS + 10 * MINUTE_IN_MILLIS);
// Say that the job ran at this point, possibly due to device idle.
// The next window should be consistent (start and end at the time it would have had the job
// run normally in previous windows).
nextWindowStartTime += 2 * HOUR_IN_MILLIS;
nextWindowEndTime += 2 * HOUR_IN_MILLIS;
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job failed but then ran
* successfully and was rescheduled when run after its expected running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_outsideWindow_failedJob() {
JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow_failedJob",
createJobInfo().setPeriodic(HOUR_IN_MILLIS));
JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job);
long now = sElapsedRealtimeClock.millis();
long nextWindowStartTime = now + HOUR_IN_MILLIS;
long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
advanceElapsedClock(HOUR_IN_MILLIS + MINUTE_IN_MILLIS);
// Say the job ran at the very end of its previous window. The intended JSS behavior is to
// have consistent windows, so the new window should start as soon as the previous window
// ended and end PERIOD time after the previous window ended.
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(2 * HOUR_IN_MILLIS);
// Say that the job ran at this point, possibly due to device idle.
// The next window should be consistent (start and end at the time it would have had the job
// run normally in previous windows).
nextWindowStartTime += 2 * HOUR_IN_MILLIS;
nextWindowEndTime += 2 * HOUR_IN_MILLIS;
rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job with a custom flex
* setting failed but then ran successfully and was rescheduled when run after its expected
* running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob() {
JobStatus job = createJobStatus(
"testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob",
createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS));
JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job);
// First window starts 30 minutes from now.
advanceElapsedClock(30 * MINUTE_IN_MILLIS);
long now = sElapsedRealtimeClock.millis();
long nextWindowStartTime = now + HOUR_IN_MILLIS;
long nextWindowEndTime = now + 90 * MINUTE_IN_MILLIS;
advanceElapsedClock(31 * MINUTE_IN_MILLIS);
// Say the job ran at the very end of its previous window. The intended JSS behavior is to
// have consistent windows, so the new window should start as soon as the previous window
// ended and end PERIOD time after the previous window ended.
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// 5 minutes before the start of the next window. It's too close to the next window, so the
// returned job should be for the window after.
advanceElapsedClock(24 * MINUTE_IN_MILLIS);
nextWindowStartTime += HOUR_IN_MILLIS;
nextWindowEndTime += HOUR_IN_MILLIS;
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(2 * HOUR_IN_MILLIS);
// Say that the job ran at this point, possibly due to device idle.
// The next window should be consistent (start and end at the time it would have had the job
// run normally in previous windows).
nextWindowStartTime += 2 * HOUR_IN_MILLIS;
nextWindowEndTime += 2 * HOUR_IN_MILLIS;
rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/** Tests that rare job batching works as expected. */
@Test
public void testRareJobBatching() {
spyOn(mService);
doNothing().when(mService).evaluateControllerStatesLocked(any());
doNothing().when(mService).noteJobsPending(any());
doReturn(true).when(mService).isReadyToBeExecutedLocked(any());
advanceElapsedClock(24 * HOUR_IN_MILLIS);
JobSchedulerService.MaybeReadyJobQueueFunctor maybeQueueFunctor =
mService.new MaybeReadyJobQueueFunctor();
mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT = 5;
mService.mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = HOUR_IN_MILLIS;
JobStatus job = createJobStatus(
"testRareJobBatching",
createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
job.setStandbyBucket(RARE_INDEX);
// Not enough RARE jobs to run.
mService.mPendingJobs.clear();
maybeQueueFunctor.reset();
for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) {
maybeQueueFunctor.accept(job);
assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
}
maybeQueueFunctor.postProcess();
assertEquals(0, mService.mPendingJobs.size());
// Enough RARE jobs to run.
mService.mPendingJobs.clear();
maybeQueueFunctor.reset();
for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT; ++i) {
maybeQueueFunctor.accept(job);
assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
}
maybeQueueFunctor.postProcess();
assertEquals(5, mService.mPendingJobs.size());
// Not enough RARE jobs to run, but a non-batched job saves the day.
mService.mPendingJobs.clear();
maybeQueueFunctor.reset();
JobStatus activeJob = createJobStatus(
"testRareJobBatching",
createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
activeJob.setStandbyBucket(ACTIVE_INDEX);
for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) {
maybeQueueFunctor.accept(job);
assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
}
maybeQueueFunctor.accept(activeJob);
maybeQueueFunctor.postProcess();
assertEquals(3, mService.mPendingJobs.size());
// Not enough RARE jobs to run, but an old RARE job saves the day.
mService.mPendingJobs.clear();
maybeQueueFunctor.reset();
JobStatus oldRareJob = createJobStatus("testRareJobBatching", createJobInfo());
oldRareJob.setStandbyBucket(RARE_INDEX);
final long oldBatchTime = sElapsedRealtimeClock.millis()
- 2 * mService.mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS;
oldRareJob.setFirstForceBatchedTimeElapsed(oldBatchTime);
for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) {
maybeQueueFunctor.accept(job);
assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
}
maybeQueueFunctor.accept(oldRareJob);
assertEquals(oldBatchTime, oldRareJob.getFirstForceBatchedTimeElapsed());
maybeQueueFunctor.postProcess();
assertEquals(3, mService.mPendingJobs.size());
}
}