Set<JobRequest> requests = new HashSet<>(manager.getAllJobRequestsForTag(builder.mTag)); for (JobRequest request : requests) { if (!request.isExact() || request.getStartMs() != JobRequest.START_NOW) { manager.cancel(request.getJobId()); .build(); if (newJob && (request.isExact() || request.isPeriodic() || request.isTransient())) { throw new IllegalArgumentException("Daily jobs cannot be exact, periodic or transient"); return request.schedule();
/** * Cancel this request if it has been scheduled. Note that if the job isn't periodic, then the * time passed since the job has been scheduled is subtracted from the time frame. For example * a job should run between 4 and 6 seconds from now. You cancel the scheduled job after 2 * seconds, then the job will run between 2 and 4 seconds after it's been scheduled again. * * @return A builder to modify the parameters. */ public Builder cancelAndEdit() { // create a temporary variable, because .cancel() will reset mScheduledAt long scheduledAt = mScheduledAt; JobManager.instance().cancel(getJobId()); Builder builder = new Builder(this.mBuilder); mStarted = false; if (!isPeriodic()) { long offset = JobConfig.getClock().currentTimeMillis() - scheduledAt; long minValue = 1L; // 1ms builder.setExecutionWindow(Math.max(minValue, getStartMs() - offset), Math.max(minValue, getEndMs() - offset)); } return builder; }
/** * @return Extra arguments for this {@link Job}. Never returns {@code null}. */ @NonNull public PersistableBundleCompat getExtras() { if (mExtras == null) { mExtras = mRequest.getExtras(); if (mExtras == null) { mExtras = new PersistableBundleCompat(); } } return mExtras; }
/** * @return Whether any of the requirements is different than the default value. */ public boolean hasRequirements() { return requiresCharging() || requiresDeviceIdle() || requiresBatteryNotLow() || requiresStorageNotLow() || requiredNetworkType() != DEFAULT_NETWORK_TYPE; }
public static long getStartMs(JobRequest request) { if (request.getFailureCount() > 0) { return request.getBackoffOffset(); } else { return request.getStartMs(); } }
@Override public String toString() { return "request{id=" + getJobId() + ", tag=" + getTag() + ", transient=" + isTransient() + '}'; }
@Test public void testPeriodic() { long interval = JobRequest.MIN_INTERVAL * 5; JobRequest request = getBuilder() .setPeriodic(interval) .setExtras(new PersistableBundleCompat()) .build(); assertThat(request.getJobId()).isGreaterThan(0); assertThat(request.getTag()).isEqualTo(DummyJobs.SuccessJob.TAG); assertThat(request.getIntervalMs()).isEqualTo(interval); assertThat(request.getFlexMs()).isEqualTo(interval); assertThat(request.isPeriodic()).isTrue(); assertThat(request.isFlexSupport()).isFalse(); assertThat(request.getStartMs()).isNegative(); assertThat(request.getEndMs()).isNegative(); assertThat(request.getBackoffMs()).isEqualTo(JobRequest.DEFAULT_BACKOFF_MS); assertThat(request.getBackoffPolicy()).isEqualTo(JobRequest.DEFAULT_BACKOFF_POLICY); assertThat(request.getExtras()).isNotNull(); assertThat(request.isExact()).isFalse(); assertThat(request.requiredNetworkType()).isEqualTo(JobRequest.DEFAULT_NETWORK_TYPE); assertThat(request.requirementsEnforced()).isFalse(); assertThat(request.requiresCharging()).isFalse(); assertThat(request.requiresDeviceIdle()).isFalse(); }
/*package*/ JobRequest reschedule(boolean failure, boolean newJob) { JobRequest newRequest = new Builder(this.mBuilder, newJob).build(); if (failure) { newRequest.mFailureCount = mFailureCount + 1; } try { newRequest.schedule(); } catch (Exception e) { CAT.e(e); // this may crash (e.g. more than 100 jobs with JobScheduler), but it's not catchable for the user, wait for reschedule } return newRequest; }
for (JobRequest request : requests) { boolean reschedule; if (request.isStarted()) { Job job = manager.getJob(request.getJobId()); reschedule = job == null; } else { reschedule = !manager.getJobProxy(request.getJobApi()).isPlatformJobScheduled(request); request.cancelAndEdit() .build() .schedule(); } catch (Exception e) {
@Test public void testAdapter() throws InterruptedException, IOException { FilePayload payload = buildPayload(); int tenMinutes = 10 * 60 * 1000; UploadRequest<FilePayload> request = buildUploadRequest(payload, tenMinutes); JobRequest adapted = AndroidJobStrategy.adapt(request); assertEquals(20, adapted.getStartMs()); assertEquals(tenMinutes, adapted.getEndMs()); assertEquals(true, adapted.requiresCharging()); assertEquals(false, adapted.requiresDeviceIdle()); assertEquals(100, adapted.getBackoffMs()); assertEquals(JobRequest.BackoffPolicy.LINEAR, adapted.getBackoffPolicy()); assertEquals(9, adapted.getExtras().get("maxErrorRetries")); } }
protected JobInfo.Builder createBaseBuilder(JobRequest request, boolean allowPersisting) { JobInfo.Builder builder = new JobInfo.Builder(request.getJobId(), new ComponentName(mContext, PlatformJobService.class)) .setRequiresCharging(request.requiresCharging()) .setRequiresDeviceIdle(request.requiresDeviceIdle()) .setRequiredNetworkType(convertNetworkType(request.requiredNetworkType())) .setPersisted(allowPersisting && !request.isTransient() && JobUtil.hasBootPermission(mContext)); return setTransientBundle(request, builder); }
@Override public void plantOneOff(JobRequest request) { PendingIntent pendingIntent = getPendingIntent(request, false); AlarmManager alarmManager = getAlarmManager(); if (alarmManager == null) { return; } try { if (request.isExact()) { if (request.getStartMs() == 1 && request.getFailureCount() <= 0) { // this job should start immediately PlatformAlarmService.start(mContext, request.getJobId(), request.getTransientExtras()); } else { plantOneOffExact(request, alarmManager, pendingIntent); } } else { plantOneOffInexact(request, alarmManager, pendingIntent); } } catch (Exception e) { // https://gist.github.com/vRallev/621b0b76a14ddde8691c mCat.e(e); } }
@Test public void testUpdateDoesNotCrash() { JobRequest request = DummyJobs.createOneOff(); int jobId = request.schedule(); assertThat(request.getScheduledAt()).isGreaterThan(0L); assertThat(request.getFailureCount()).isEqualTo(0); assertThat(request.getLastRun()).isEqualTo(0); SQLiteDatabase database = mock(SQLiteDatabase.class); when(database.update(anyString(), any(ContentValues.class), nullable(String.class), any(String[].class))).thenThrow(SQLException.class); manager().getJobStorage().injectDatabase(database); request.updateStats(true, true); // updates the database value, but fails in this case assertThat(request.getFailureCount()).isEqualTo(1); // in memory value was updated, keep that assertThat(request.getLastRun()).isGreaterThan(0); // kinda hacky, this removes the request from the cache, but doesn't delete it in the database, // because we're using the mock at the moment manager().getJobStorage().remove(request); manager().getJobStorage().injectDatabase(null); // reset request = manager().getJobRequest(jobId); assertThat(request.getFailureCount()).isEqualTo(0); assertThat(request.getLastRun()).isEqualTo(0); }
@Test public void testSameIdAfterCancel() { JobRequest request = DummyJobs.createOneOff(); int jobId = request.getJobId(); assertThat(request.getScheduledAt()).isEqualTo(0L); manager().schedule(request); assertThat(request.getScheduledAt()).isGreaterThan(0L); JobRequest requestNew = request.cancelAndEdit().build(); assertThat(request.getScheduledAt()).isEqualTo(0L); int newId = requestNew.schedule(); assertThat(newId).isEqualTo(jobId); assertThat(request.getScheduledAt()).isEqualTo(0L); assertThat(requestNew.getScheduledAt()).isGreaterThan(0L); }
public static long getEndMs(JobRequest request, boolean shiftEnd) { long endMs; if (request.getFailureCount() > 0) { endMs = request.getBackoffOffset(); } else { endMs = request.getEndMs(); } if (shiftEnd && request.requirementsEnforced() && request.hasRequirements()) { // move the end backwards if the API is smart with the requirements endMs = checkedMultiply(endMs, 100); } return endMs; }
@Test public void testFlex() { JobConfig.forceApi(JobApi.V_14); long interval = JobRequest.MIN_INTERVAL * 5; long flex = JobRequest.MIN_FLEX * 5; JobRequest request = getBuilder() .setPeriodic(interval, flex) .build(); JobManager.instance().schedule(request); assertThat(request.getJobId()).isGreaterThan(0); assertThat(request.getTag()).isEqualTo(DummyJobs.SuccessJob.TAG); assertThat(request.getIntervalMs()).isEqualTo(interval); assertThat(request.getFlexMs()).isEqualTo(flex); assertThat(request.isPeriodic()).isTrue(); assertThat(request.isFlexSupport()).isTrue(); }
/** * {@inheritDoc} */ @Override public void executeRequestsNow(int howMany) { int started = 0; for (JobRequest jobRequest : JobManager.instance().getAllJobRequests()) { if (isSoonButNotImmediate(jobRequest)) { JobRequest.Builder builder = jobRequest.cancelAndEdit(); long endMillis = Math.max(jobRequest.getEndMs(), RUN_NOW_TIME_WINDOW_END); builder.setExecutionWindow(RUN_NOW_TIME_WINDOW_START, endMillis).build().schedule(); started++; } if (started == howMany) { break; } } Logger.d(TAG, String.format("Job scheduled started %d requests.", started)); }
@SuppressWarnings("ConstantConditions") @Test public void verifyEarlyExecution() { TestClock clock = new TestClock(); clock.setTime(13, 0); JobRequest request = verifyExecutionAndSuccessfulReschedule(clock, TimeUnit.HOURS.toMillis(14), TimeUnit.HOURS.toMillis(15)); assertThat(request.getStartMs()).isEqualTo(TimeUnit.HOURS.toMillis(25)); assertThat(request.getEndMs()).isEqualTo(TimeUnit.HOURS.toMillis(26)); int id = DailyJob.schedule(DummyJobs.createBuilder(DummyJobs.SuccessDailyJob.class), TimeUnit.HOURS.toMillis(14), TimeUnit.HOURS.toMillis(15)); request = manager().getJobRequest(id); assertThat(request.getStartMs()).isEqualTo(TimeUnit.HOURS.toMillis(1)); assertThat(request.getEndMs()).isEqualTo(TimeUnit.HOURS.toMillis(2)); }
@Override public void plantOneOff(JobRequest request) { if (request.isTransient()) { TransientBundleHolder.putBundle(request.getJobId(), request.getTransientExtras()); } OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(PlatformWorker.class) .setInitialDelay(request.getStartMs(), TimeUnit.MILLISECONDS) // don't use the average here, WorkManager will do the right thing .setConstraints(buildConstraints(request)) .addTag(createTag(request.getJobId())) .build(); // don't set the back-off criteria, android-job is handling this WorkManager workManager = getWorkManager(); if (workManager == null) { throw new JobProxyIllegalStateException("WorkManager is null"); } workManager.enqueue(workRequest); }
/*package*/ long getBackoffOffset() { if (isPeriodic()) { return 0L; } long offset; switch (getBackoffPolicy()) { case LINEAR: offset = mFailureCount * getBackoffMs(); break; case EXPONENTIAL: if (mFailureCount == 0) { offset = 0L; } else { offset = (long) (getBackoffMs() * Math.pow(2, mFailureCount - 1)); } break; default: throw new IllegalStateException("not implemented"); } return Math.min(offset, TimeUnit.HOURS.toMillis(5)); // use max of 5 hours like JobScheduler }