BatchingScheduler.java
package com.birbit.android.jobqueue;
import android.content.Context;
import android.support.annotation.Nullable;
import com.birbit.android.jobqueue.scheduling.Scheduler;
import com.birbit.android.jobqueue.scheduling.SchedulerConstraint;
import com.birbit.android.jobqueue.timer.Timer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* JobManager calls scheduler every time it receives some job that can use the Scheduler APIs.
* This may get too noisy and unnecessary.
* <p>
* This BatchingScheduler wraps a generic scheduler and avoid calling the system service if a
* request is made that has the same criteria as the previous one.
*/
public class BatchingScheduler extends Scheduler {
// batch by 15 min intervals
public static final long DEFAULT_BATCHING_PERIOD_IN_MS = TimeUnit.SECONDS.toMillis(60 * 15);
private long batchingDurationInMs = DEFAULT_BATCHING_PERIOD_IN_MS;
private long batchingDurationInNs = TimeUnit.MILLISECONDS.toNanos(batchingDurationInMs);
private final Scheduler delegate;
private final List<ConstraintWrapper> constraints = new ArrayList<>();
private final Timer timer;
public BatchingScheduler(Scheduler delegate, Timer timer) {
this.delegate = delegate;
this.timer = timer;
}
@Override
public void init(Context context, Callback callback) {
super.init(context, callback);
delegate.init(context, new Callback() {
@Override
public boolean start(SchedulerConstraint constraint) {
removeFromConstraints(constraint);
return BatchingScheduler.this.start(constraint);
}
@Override
public boolean stop(SchedulerConstraint constraint) {
return BatchingScheduler.this.stop(constraint);
}
});
}
private void removeFromConstraints(SchedulerConstraint constraint) {
synchronized (constraints) {
for (int i = constraints.size() - 1; i >= 0; i--) {
ConstraintWrapper existing = constraints.get(i);
if (existing.constraint.getUuid().equals(constraint.getUuid())) {
constraints.remove(i);
}
}
}
}
private boolean addToConstraints(SchedulerConstraint constraint) {
final long now = timer.nanoTime();
long expectedRunTime = TimeUnit.MILLISECONDS.toNanos(constraint.getDelayInMs()) + now;
Long expectedDeadline = constraint.getOverrideDeadlineInMs() == null
? null
: TimeUnit.MILLISECONDS.toNanos(constraint.getOverrideDeadlineInMs()) + now;
synchronized (constraints) {
for (ConstraintWrapper existing : constraints) {
if (covers(existing, constraint, expectedRunTime, expectedDeadline)) {
return false;
}
}
// fix the delay
long group = constraint.getDelayInMs() / batchingDurationInMs;
long newDelay = (group + 1) * batchingDurationInMs;
constraint.setDelayInMs(newDelay);
Long deadline = null;
if (constraint.getOverrideDeadlineInMs() != null) {
group = constraint.getOverrideDeadlineInMs() / batchingDurationInMs;
deadline = (group + 1) * batchingDurationInMs;
constraint.setOverrideDeadlineInMs(deadline);
}
constraints.add(new ConstraintWrapper(now + TimeUnit.MILLISECONDS.toNanos(newDelay),
deadline == null ? null : now + TimeUnit.MILLISECONDS.toNanos(deadline), constraint));
return true;
}
}
private boolean covers(ConstraintWrapper existing, SchedulerConstraint constraint,
long expectedRunTime, Long expectedDeadline) {
if (existing.constraint.getNetworkStatus() != constraint.getNetworkStatus()) {
return false;
}
if (expectedDeadline != null) {
if (existing.deadlineNs == null) {
return false;
}
long timeDiff = existing.deadlineNs - expectedDeadline;
if (timeDiff < 1 || timeDiff > batchingDurationInNs) {
return false;
}
} else if (existing.deadlineNs != null) {
return false;
}
// same network status, check if time matches
long timeDiff = existing.delayUntilNs - expectedRunTime;
return timeDiff > 0 && timeDiff <= batchingDurationInNs;
}
@Override
public void request(SchedulerConstraint constraint) {
if (addToConstraints(constraint)) {
delegate.request(constraint);
}
}
@Override
public void onFinished(SchedulerConstraint constraint, boolean reschedule) {
removeFromConstraints(constraint);
delegate.onFinished(constraint, false);
if (reschedule) {
request(constraint);
}
}
@Override
public void cancelAll() {
synchronized (constraints) {
constraints.clear();
}
delegate.cancelAll();
}
private static class ConstraintWrapper {
final long delayUntilNs;
@Nullable final Long deadlineNs;
final SchedulerConstraint constraint;
public ConstraintWrapper(long delayUntilNs, Long deadlineNs, SchedulerConstraint constraint) {
this.delayUntilNs = delayUntilNs;
this.deadlineNs = deadlineNs;
this.constraint = constraint;
}
}
}