2018-06-18 12:27:04 -07:00
|
|
|
package org.thoughtcrime.securesms.jobmanager;
|
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
import android.app.Application;
|
|
|
|
import android.content.Intent;
|
|
|
|
import android.os.Build;
|
2020-08-19 10:06:26 +10:00
|
|
|
import androidx.annotation.NonNull;
|
2018-08-09 10:15:43 -04:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
import org.thoughtcrime.securesms.jobmanager.impl.DefaultExecutorFactory;
|
|
|
|
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
|
|
|
import org.thoughtcrime.securesms.jobmanager.migration.WorkManagerMigrator;
|
|
|
|
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
|
2018-11-27 12:34:42 -08:00
|
|
|
import org.thoughtcrime.securesms.logging.Log;
|
2019-03-28 08:56:35 -07:00
|
|
|
import org.thoughtcrime.securesms.util.Debouncer;
|
2018-11-27 12:34:42 -08:00
|
|
|
|
2018-12-07 12:16:37 -08:00
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.Collections;
|
2019-03-28 08:56:35 -07:00
|
|
|
import java.util.HashMap;
|
2018-12-07 12:16:37 -08:00
|
|
|
import java.util.LinkedList;
|
|
|
|
import java.util.List;
|
2019-03-28 08:56:35 -07:00
|
|
|
import java.util.Map;
|
|
|
|
import java.util.Set;
|
|
|
|
import java.util.concurrent.CopyOnWriteArraySet;
|
2018-11-27 12:34:42 -08:00
|
|
|
import java.util.concurrent.ExecutionException;
|
2019-03-28 08:56:35 -07:00
|
|
|
import java.util.concurrent.ExecutorService;
|
|
|
|
import java.util.concurrent.Future;
|
2018-06-18 12:27:04 -07:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
/**
|
|
|
|
* Allows the scheduling of durable jobs that will be run as early as possible.
|
|
|
|
*/
|
|
|
|
public class JobManager implements ConstraintObserver.Notifier {
|
2018-06-18 12:27:04 -07:00
|
|
|
|
2018-11-27 12:34:42 -08:00
|
|
|
private static final String TAG = JobManager.class.getSimpleName();
|
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
private final ExecutorService executor;
|
|
|
|
private final JobController jobController;
|
|
|
|
private final JobRunner[] jobRunners;
|
|
|
|
|
|
|
|
private final Set<EmptyQueueListener> emptyQueueListeners = new CopyOnWriteArraySet<>();
|
|
|
|
|
|
|
|
public JobManager(@NonNull Application application, @NonNull Configuration configuration) {
|
|
|
|
this.executor = configuration.getExecutorFactory().newSingleThreadExecutor("JobManager");
|
|
|
|
this.jobRunners = new JobRunner[configuration.getJobThreadCount()];
|
|
|
|
this.jobController = new JobController(application,
|
|
|
|
configuration.getJobStorage(),
|
|
|
|
configuration.getJobInstantiator(),
|
|
|
|
configuration.getConstraintFactories(),
|
|
|
|
configuration.getDataSerializer(),
|
|
|
|
configuration.getDependencyInjector(),
|
|
|
|
Build.VERSION.SDK_INT < 26 ? new AlarmManagerScheduler(application)
|
|
|
|
: new CompositeScheduler(new InAppScheduler(this), new JobSchedulerScheduler(application)),
|
|
|
|
new Debouncer(500),
|
|
|
|
this::onEmptyQueue);
|
|
|
|
|
|
|
|
executor.execute(() -> {
|
|
|
|
if (WorkManagerMigrator.needsMigration(application)) {
|
|
|
|
Log.i(TAG, "Detected an old WorkManager database. Migrating.");
|
|
|
|
WorkManagerMigrator.migrate(application, configuration.getJobStorage(), configuration.getDataSerializer());
|
|
|
|
}
|
2018-06-18 12:27:04 -07:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
jobController.init();
|
2018-06-18 12:27:04 -07:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
for (int i = 0; i < jobRunners.length; i++) {
|
|
|
|
jobRunners[i] = new JobRunner(application, i + 1, jobController);
|
|
|
|
jobRunners[i].start();
|
|
|
|
}
|
2018-06-18 12:27:04 -07:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
for (ConstraintObserver constraintObserver : configuration.getConstraintObservers()) {
|
|
|
|
constraintObserver.register(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT < 26) {
|
|
|
|
application.startService(new Intent(application, KeepAliveService.class));
|
|
|
|
}
|
|
|
|
|
|
|
|
wakeUp();
|
|
|
|
});
|
2018-06-18 12:27:04 -07:00
|
|
|
}
|
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
/**
|
|
|
|
* Enqueues a single job to be run.
|
|
|
|
*/
|
|
|
|
public void add(@NonNull Job job) {
|
|
|
|
new Chain(this, Collections.singletonList(job)).enqueue();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Begins the creation of a job chain with a single job.
|
|
|
|
* @see Chain
|
|
|
|
*/
|
2018-12-07 12:16:37 -08:00
|
|
|
public Chain startChain(@NonNull Job job) {
|
2019-03-28 08:56:35 -07:00
|
|
|
return new Chain(this, Collections.singletonList(job));
|
2018-12-07 12:16:37 -08:00
|
|
|
}
|
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
/**
|
|
|
|
* Begins the creation of a job chain with a set of jobs that can be run in parallel.
|
|
|
|
* @see Chain
|
|
|
|
*/
|
2018-12-07 12:16:37 -08:00
|
|
|
public Chain startChain(@NonNull List<? extends Job> jobs) {
|
2019-03-28 08:56:35 -07:00
|
|
|
return new Chain(this, jobs);
|
2018-12-07 12:16:37 -08:00
|
|
|
}
|
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
/**
|
|
|
|
* Retrieves a string representing the state of the job queue. Intended for debugging.
|
|
|
|
*/
|
|
|
|
public @NonNull String getDebugInfo() {
|
|
|
|
Future<String> result = executor.submit(jobController::getDebugInfo);
|
|
|
|
try {
|
|
|
|
return result.get();
|
|
|
|
} catch (ExecutionException | InterruptedException e) {
|
|
|
|
Log.w(TAG, "Failed to retrieve Job info.", e);
|
|
|
|
return "Failed to retrieve Job info.";
|
2018-12-07 12:16:37 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
/**
|
|
|
|
* Adds a listener to that will be notified when the job queue has been drained.
|
|
|
|
*/
|
|
|
|
void addOnEmptyQueueListener(@NonNull EmptyQueueListener listener) {
|
2018-08-09 10:15:43 -04:00
|
|
|
executor.execute(() -> {
|
2019-03-28 08:56:35 -07:00
|
|
|
emptyQueueListeners.add(listener);
|
|
|
|
});
|
|
|
|
}
|
2018-06-18 12:27:04 -07:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
/**
|
|
|
|
* Removes a listener that was added via {@link #addOnEmptyQueueListener(EmptyQueueListener)}.
|
|
|
|
*/
|
|
|
|
void removeOnEmptyQueueListener(@NonNull EmptyQueueListener listener) {
|
|
|
|
executor.execute(() -> {
|
|
|
|
emptyQueueListeners.remove(listener);
|
|
|
|
});
|
|
|
|
}
|
2018-06-18 12:27:04 -07:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
@Override
|
|
|
|
public void onConstraintMet(@NonNull String reason) {
|
|
|
|
Log.i(TAG, "onConstraintMet(" + reason + ")");
|
|
|
|
wakeUp();
|
|
|
|
}
|
2018-06-18 12:27:04 -07:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
/**
|
|
|
|
* Pokes the system to take another pass at the job queue.
|
|
|
|
*/
|
|
|
|
void wakeUp() {
|
|
|
|
executor.execute(jobController::wakeUp);
|
|
|
|
}
|
2018-06-18 12:27:04 -07:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
private void enqueueChain(@NonNull Chain chain) {
|
|
|
|
executor.execute(() -> {
|
|
|
|
jobController.submitNewJobChain(chain.getJobListChain());
|
|
|
|
wakeUp();
|
|
|
|
});
|
|
|
|
}
|
2018-12-07 12:16:37 -08:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
private void onEmptyQueue() {
|
|
|
|
executor.execute(() -> {
|
|
|
|
for (EmptyQueueListener listener : emptyQueueListeners) {
|
|
|
|
listener.onQueueEmpty();
|
2018-12-07 12:16:37 -08:00
|
|
|
}
|
2018-08-09 10:15:43 -04:00
|
|
|
});
|
2019-03-28 08:56:35 -07:00
|
|
|
}
|
2018-12-07 12:16:37 -08:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
public interface EmptyQueueListener {
|
|
|
|
void onQueueEmpty();
|
2018-12-07 12:16:37 -08:00
|
|
|
}
|
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
/**
|
|
|
|
* Allows enqueuing work that depends on each other. Jobs that appear later in the chain will
|
|
|
|
* only run after all jobs earlier in the chain have been completed. If a job fails, all jobs
|
|
|
|
* that occur later in the chain will also be failed.
|
|
|
|
*/
|
|
|
|
public static class Chain {
|
|
|
|
|
|
|
|
private final JobManager jobManager;
|
|
|
|
private final List<List<Job>> jobs;
|
2018-12-07 12:16:37 -08:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
private Chain(@NonNull JobManager jobManager, @NonNull List<? extends Job> jobs) {
|
|
|
|
this.jobManager = jobManager;
|
|
|
|
this.jobs = new LinkedList<>();
|
|
|
|
|
|
|
|
this.jobs.add(new ArrayList<>(jobs));
|
2018-12-07 12:16:37 -08:00
|
|
|
}
|
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
public Chain then(@NonNull Job job) {
|
|
|
|
return then(Collections.singletonList(job));
|
|
|
|
}
|
2018-12-07 12:16:37 -08:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
public Chain then(@NonNull List<Job> jobs) {
|
|
|
|
if (!jobs.isEmpty()) {
|
|
|
|
this.jobs.add(new ArrayList<>(jobs));
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
2018-12-07 12:16:37 -08:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
public void enqueue() {
|
|
|
|
jobManager.enqueueChain(this);
|
2018-12-07 12:16:37 -08:00
|
|
|
}
|
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
private List<List<Job>> getJobListChain() {
|
|
|
|
return jobs;
|
|
|
|
}
|
2018-12-07 12:16:37 -08:00
|
|
|
}
|
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
public static class Configuration {
|
|
|
|
|
|
|
|
private final ExecutorFactory executorFactory;
|
|
|
|
private final int jobThreadCount;
|
|
|
|
private final JobInstantiator jobInstantiator;
|
|
|
|
private final ConstraintInstantiator constraintInstantiator;
|
|
|
|
private final List<ConstraintObserver> constraintObservers;
|
|
|
|
private final Data.Serializer dataSerializer;
|
|
|
|
private final JobStorage jobStorage;
|
|
|
|
private final DependencyInjector dependencyInjector;
|
|
|
|
|
|
|
|
private Configuration(int jobThreadCount,
|
|
|
|
@NonNull ExecutorFactory executorFactory,
|
|
|
|
@NonNull JobInstantiator jobInstantiator,
|
|
|
|
@NonNull ConstraintInstantiator constraintInstantiator,
|
|
|
|
@NonNull List<ConstraintObserver> constraintObservers,
|
|
|
|
@NonNull Data.Serializer dataSerializer,
|
|
|
|
@NonNull JobStorage jobStorage,
|
|
|
|
@NonNull DependencyInjector dependencyInjector)
|
|
|
|
{
|
|
|
|
this.executorFactory = executorFactory;
|
|
|
|
this.jobThreadCount = jobThreadCount;
|
|
|
|
this.jobInstantiator = jobInstantiator;
|
|
|
|
this.constraintInstantiator = constraintInstantiator;
|
|
|
|
this.constraintObservers = constraintObservers;
|
|
|
|
this.dataSerializer = dataSerializer;
|
|
|
|
this.jobStorage = jobStorage;
|
|
|
|
this.dependencyInjector = dependencyInjector;
|
|
|
|
}
|
2018-12-07 12:16:37 -08:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
int getJobThreadCount() {
|
|
|
|
return jobThreadCount;
|
|
|
|
}
|
2018-12-07 12:16:37 -08:00
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
@NonNull ExecutorFactory getExecutorFactory() {
|
|
|
|
return executorFactory;
|
2018-12-07 12:16:37 -08:00
|
|
|
}
|
|
|
|
|
2019-04-17 10:21:30 -04:00
|
|
|
@NonNull JobInstantiator getJobInstantiator() {
|
2019-03-28 08:56:35 -07:00
|
|
|
return jobInstantiator;
|
2018-12-07 12:16:37 -08:00
|
|
|
}
|
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
@NonNull
|
|
|
|
ConstraintInstantiator getConstraintFactories() {
|
|
|
|
return constraintInstantiator;
|
2018-12-07 12:16:37 -08:00
|
|
|
}
|
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
@NonNull List<ConstraintObserver> getConstraintObservers() {
|
|
|
|
return constraintObservers;
|
2018-12-07 12:16:37 -08:00
|
|
|
}
|
|
|
|
|
2019-03-28 08:56:35 -07:00
|
|
|
@NonNull Data.Serializer getDataSerializer() {
|
|
|
|
return dataSerializer;
|
|
|
|
}
|
|
|
|
|
|
|
|
@NonNull JobStorage getJobStorage() {
|
|
|
|
return jobStorage;
|
|
|
|
}
|
|
|
|
|
|
|
|
@NonNull DependencyInjector getDependencyInjector() {
|
|
|
|
return dependencyInjector;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static class Builder {
|
|
|
|
|
|
|
|
private ExecutorFactory executorFactory = new DefaultExecutorFactory();
|
|
|
|
private int jobThreadCount = Math.max(2, Math.min(Runtime.getRuntime().availableProcessors() - 1, 4));
|
|
|
|
private Map<String, Job.Factory> jobFactories = new HashMap<>();
|
|
|
|
private Map<String, Constraint.Factory> constraintFactories = new HashMap<>();
|
|
|
|
private List<ConstraintObserver> constraintObservers = new ArrayList<>();
|
|
|
|
private Data.Serializer dataSerializer = new JsonDataSerializer();
|
|
|
|
private JobStorage jobStorage = null;
|
|
|
|
private DependencyInjector dependencyInjector = o -> { /*noop*/ };
|
|
|
|
|
|
|
|
public @NonNull Builder setJobThreadCount(int jobThreadCount) {
|
|
|
|
this.jobThreadCount = jobThreadCount;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public @NonNull Builder setExecutorFactory(@NonNull ExecutorFactory executorFactory) {
|
|
|
|
this.executorFactory = executorFactory;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public @NonNull Builder setJobFactories(@NonNull Map<String, Job.Factory> jobFactories) {
|
|
|
|
this.jobFactories = jobFactories;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public @NonNull Builder setConstraintFactories(@NonNull Map<String, Constraint.Factory> constraintFactories) {
|
|
|
|
this.constraintFactories = constraintFactories;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public @NonNull Builder setConstraintObservers(@NonNull List<ConstraintObserver> constraintObservers) {
|
|
|
|
this.constraintObservers = constraintObservers;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public @NonNull Builder setDataSerializer(@NonNull Data.Serializer dataSerializer) {
|
|
|
|
this.dataSerializer = dataSerializer;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public @NonNull Builder setJobStorage(@NonNull JobStorage jobStorage) {
|
|
|
|
this.jobStorage = jobStorage;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public @NonNull Builder setDependencyInjector(@NonNull DependencyInjector dependencyInjector) {
|
|
|
|
this.dependencyInjector = dependencyInjector;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public @NonNull Configuration build() {
|
|
|
|
return new Configuration(jobThreadCount,
|
|
|
|
executorFactory,
|
|
|
|
new JobInstantiator(jobFactories),
|
|
|
|
new ConstraintInstantiator(constraintFactories),
|
|
|
|
new ArrayList<>(constraintObservers),
|
|
|
|
dataSerializer,
|
|
|
|
jobStorage,
|
|
|
|
dependencyInjector);
|
|
|
|
}
|
2018-12-07 12:16:37 -08:00
|
|
|
}
|
2018-08-09 10:15:43 -04:00
|
|
|
}
|
2018-06-18 12:27:04 -07:00
|
|
|
}
|