Schedule jobs with WorkManager.

Should help solve most of our pressing targetSdk=26 migration issues.
This commit is contained in:
Greyson Parrelli
2018-08-09 10:15:43 -04:00
parent d10a44f8eb
commit 87e6aa48bb
55 changed files with 1442 additions and 1192 deletions

View File

@@ -1,138 +1,139 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.jobmanager;
import android.os.PowerManager;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.jobmanager.requirements.Requirement;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.jobs.requirements.SqlCipherMigrationRequirement;
import org.thoughtcrime.securesms.logging.Log;
import java.io.Serializable;
import java.util.List;
import java.util.UUID;
/**
* An abstract class representing a unit of work that can be scheduled with
* the JobManager. This should be extended to implement tasks.
*/
public abstract class Job implements Serializable {
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
private final JobParameters parameters;
public abstract class Job extends Worker implements Serializable {
private transient long persistentId;
private transient int runIteration;
private transient long lastRunTime;
private transient PowerManager.WakeLock wakeLock;
private static final long serialVersionUID = -4658540468214421276L;
public Job(JobParameters parameters) {
this.parameters = parameters;
}
private static final String TAG = Job.class.getSimpleName();
public List<Requirement> getRequirements() {
return parameters.getRequirements();
}
static final String KEY_RETRY_COUNT = "Job_retry_count";
static final String KEY_RETRY_UNTIL = "Job_retry_until";
static final String KEY_SUBMIT_TIME = "Job_submit_time";
static final String KEY_REQUIRES_MASTER_SECRET = "Job_requires_master_secret";
static final String KEY_REQUIRES_SQLCIPHER = "Job_requires_sqlcipher";
public boolean isRequirementsMet() {
for (Requirement requirement : parameters.getRequirements()) {
if (!requirement.isPresent(this)) return false;
}
private JobParameters parameters;
return true;
}
public String getGroupId() {
return parameters.getGroupId();
}
public boolean isPersistent() {
return parameters.isPersistent();
}
public EncryptionKeys getEncryptionKeys() {
return parameters.getEncryptionKeys();
}
public void setEncryptionKeys(EncryptionKeys keys) {
parameters.setEncryptionKeys(keys);
}
public int getRetryCount() {
return parameters.getRetryCount();
}
public long getRetryUntil() {
return parameters.getRetryUntil();
}
public long getLastRunTime() {
return lastRunTime;
}
public void resetRunStats() {
runIteration = 0;
lastRunTime = 0;
}
public void setPersistentId(long persistentId) {
this.persistentId = persistentId;
}
public long getPersistentId() {
return persistentId;
}
public int getRunIteration() {
return runIteration;
}
public boolean needsWakeLock() {
return parameters.needsWakeLock();
}
public long getWakeLockTimeout() {
return parameters.getWakeLockTimeout();
}
public void setWakeLock(PowerManager.WakeLock wakeLock) {
this.wakeLock = wakeLock;
}
public PowerManager.WakeLock getWakeLock() {
return this.wakeLock;
}
public void onRetry() {
runIteration++;
lastRunTime = System.currentTimeMillis();
for (Requirement requirement : parameters.getRequirements()) {
requirement.onRetry(this);
}
public Job(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
/**
* Called after a job has been added to the JobManager queue. If it's a persistent job,
* the state has been persisted to disk before this method is called.
* Invoked when a job is first created in our own codebase.
*/
public abstract void onAdded();
protected Job(@Nullable JobParameters parameters) {
this.parameters = parameters;
}
@NonNull
@Override
public Result doWork() {
Data data = getInputData();
log("doWork()" + logSuffix());
ApplicationContext.getInstance(getApplicationContext()).ensureInitialized();
ApplicationContext.getInstance(getApplicationContext()).injectDependencies(this);
if (this instanceof ContextDependent) {
((ContextDependent)this).setContext(getApplicationContext());
}
initialize(new SafeData(data));
try {
if (withinRetryLimits(data)) {
if (requirementsMet(data)) {
onRun();
log("Successfully completed." + logSuffix());
return Result.SUCCESS;
} else {
log("Retrying due to unmet requirements." + logSuffix());
return retry();
}
} else {
warn("Failing after hitting the retry limit." + logSuffix());
return cancel();
}
} catch (Exception e) {
if (onShouldRetry(e)) {
log("Retrying after a retryable exception." + logSuffix());
return retry();
}
warn("Failing due to an exception." + logSuffix(), e);
return cancel();
}
}
@Override
public void onStopped(boolean cancelled) {
if (cancelled) {
warn("onStopped() with cancellation signal." + logSuffix());
onCanceled();
}
}
final void onSubmit(UUID id) {
log(id, "onSubmit()");
onAdded();
}
/**
* Called after a run has finished and we've determined a retry is required, but before the next
* attempt is run.
*/
protected void onRetry() { }
/**
* Called after a job has been added to the JobManager queue. Invoked off the main thread, so its
* safe to do longer-running work. However, work should finish relatively quickly, as it will
* block the submission of future tasks.
*/
protected void onAdded() { }
/**
* All instance state needs to be persisted in the provided {@link Data.Builder} so that it can
* be restored in {@link #initialize(SafeData)}.
* @param dataBuilder The builder where you put your state.
* @return The result of {@code dataBuilder.build()}.
*/
protected abstract @NonNull Data serialize(@NonNull Data.Builder dataBuilder);
/**
* Restore all of your instance state from the provided {@link Data}. It should contain all of
* the data put in during {@link #serialize(Data.Builder)}.
* @param data Where your data is stored.
*/
protected abstract void initialize(@NonNull SafeData data);
/**
* Called to actually execute the job.
* @throws Exception
*/
protected abstract void onRun() throws Exception;
public abstract void onRun() throws Exception;
/**
* Called if a job fails to run (onShouldRetry returned false, or the number of retries exceeded
* the job's configured retry count.
*/
protected abstract void onCanceled();
/**
* If onRun() throws an exception, this method will be called to determine whether the
@@ -141,14 +142,69 @@ public abstract class Job implements Serializable {
* @param exception The exception onRun() threw.
* @return true if onRun() should be called again, false otherwise.
*/
public abstract boolean onShouldRetry(Exception exception);
protected abstract boolean onShouldRetry(Exception exception);
/**
* Called if a job fails to run (onShouldRetry returned false, or the number of retries exceeded
* the job's configured retry count.
*/
public abstract void onCanceled();
@Nullable JobParameters getJobParameters() {
return parameters;
}
private Result retry() {
onRetry();
return Result.RETRY;
}
private Result cancel() {
onCanceled();
return Result.SUCCESS;
}
private boolean requirementsMet(Data data) {
boolean met = true;
if (data.getBoolean(KEY_REQUIRES_MASTER_SECRET, false)) {
met &= new MasterSecretRequirement(getApplicationContext()).isPresent();
}
if (data.getBoolean(KEY_REQUIRES_SQLCIPHER, false)) {
met &= new SqlCipherMigrationRequirement(getApplicationContext()).isPresent();
}
return met;
}
private boolean withinRetryLimits(Data data) {
int retryCount = data.getInt(KEY_RETRY_COUNT, 0);
long retryUntil = data.getLong(KEY_RETRY_UNTIL, 0);
if (retryCount > 0) {
return getRunAttemptCount() <= retryCount;
}
return System.currentTimeMillis() < retryUntil;
}
private void log(@NonNull String message) {
log(getId(), message);
}
private void log(@NonNull UUID id, @NonNull String message) {
Log.i(TAG, buildLog(id, message));
}
private void warn(@NonNull String message) {
warn(message, null);
}
private void warn(@NonNull String message, @Nullable Throwable t) {
Log.w(TAG, buildLog(getId(), message), t);
}
private String buildLog(@NonNull UUID id, @NonNull String message) {
return "[" + id + "] " + getClass().getSimpleName() + " :: " + message;
}
private String logSuffix() {
long timeSinceSubmission = System.currentTimeMillis() - getInputData().getLong(KEY_SUBMIT_TIME, 0);
return " (Time since submission: " + timeSinceSubmission + " ms, Run attempt: " + getRunAttemptCount() + ")";
}
}

View File

@@ -1,100 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.jobmanager;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.jobmanager.persistence.PersistentStorage;
class JobConsumer extends Thread {
private static final String TAG = JobConsumer.class.getSimpleName();
enum JobResult {
SUCCESS,
FAILURE,
DEFERRED
}
private final JobQueue jobQueue;
private final PersistentStorage persistentStorage;
public JobConsumer(String name, JobQueue jobQueue, PersistentStorage persistentStorage) {
super(name);
this.jobQueue = jobQueue;
this.persistentStorage = persistentStorage;
}
@Override
public void run() {
while (true) {
Job job = jobQueue.getNext();
JobResult result = runJob(job);
if (result == JobResult.DEFERRED) {
jobQueue.push(job);
} else {
if (result == JobResult.FAILURE) {
job.onCanceled();
}
if (job.isPersistent()) {
persistentStorage.remove(job.getPersistentId());
}
if (job.getWakeLock() != null && job.getWakeLockTimeout() == 0) {
job.getWakeLock().release();
}
if (job.getGroupId() != null) {
jobQueue.setGroupIdAvailable(job.getGroupId());
}
}
}
}
private JobResult runJob(Job job) {
while (canRetry(job)) {
try {
job.onRun();
return JobResult.SUCCESS;
} catch (Exception exception) {
Log.w(TAG, exception);
if (exception instanceof RuntimeException) {
throw (RuntimeException)exception;
} else if (!job.onShouldRetry(exception)) {
return JobResult.FAILURE;
}
job.onRetry();
if (!job.isRequirementsMet()) {
return JobResult.DEFERRED;
}
}
}
return JobResult.FAILURE;
}
private boolean canRetry(@NonNull Job job) {
if (job.getRetryCount() > 0) {
return job.getRunIteration() < job.getRetryCount();
}
return System.currentTimeMillis() < job.getRetryUntil();
}
}

View File

@@ -1,252 +1,69 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.jobmanager;
import android.content.Context;
import android.os.PowerManager;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.jobmanager.dependencies.AggregateDependencyInjector;
import org.thoughtcrime.securesms.jobmanager.dependencies.DependencyInjector;
import org.thoughtcrime.securesms.jobmanager.persistence.JobSerializer;
import org.thoughtcrime.securesms.jobmanager.persistence.PersistentStorage;
import org.thoughtcrime.securesms.jobmanager.requirements.RequirementListener;
import org.thoughtcrime.securesms.jobmanager.requirements.RequirementProvider;
import org.thoughtcrime.securesms.logging.Log;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.TimeUnit;
/**
* A JobManager allows you to enqueue {@link org.thoughtcrime.securesms.jobmanager.Job} tasks
* that are executed once a Job's {@link org.thoughtcrime.securesms.jobmanager.requirements.Requirement}s
* are met.
*/
public class JobManager implements RequirementListener {
import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
private final JobQueue jobQueue = new JobQueue();
private final Executor eventExecutor = Executors.newSingleThreadExecutor();
private final AtomicBoolean hasLoadedEncrypted = new AtomicBoolean(false);
public class JobManager {
private final Context context;
private final PersistentStorage persistentStorage;
private final List<RequirementProvider> requirementProviders;
private final AggregateDependencyInjector dependencyInjector;
private static final Constraints NETWORK_CONSTRAINT = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
private JobManager(Context context, String name,
List<RequirementProvider> requirementProviders,
DependencyInjector dependencyInjector,
JobSerializer jobSerializer, int consumers)
{
this.context = context;
this.dependencyInjector = new AggregateDependencyInjector(dependencyInjector);
this.persistentStorage = new PersistentStorage(context, name, jobSerializer, this.dependencyInjector);
this.requirementProviders = requirementProviders;
private final Executor executor = Executors.newSingleThreadExecutor();
eventExecutor.execute(new LoadTask(null));
private final WorkManager workManager;
if (requirementProviders != null && !requirementProviders.isEmpty()) {
for (RequirementProvider provider : requirementProviders) {
provider.setListener(this);
public JobManager(@NonNull WorkManager workManager) {
this.workManager = workManager;
}
public void add(Job job) {
executor.execute(() -> {
workManager.synchronous().pruneWorkSync();
JobParameters jobParameters = job.getJobParameters();
if (jobParameters == null) {
throw new IllegalStateException("Jobs must have JobParameters at this stage.");
}
}
for (int i=0;i<consumers;i++) {
new JobConsumer("JobConsumer-" + i, jobQueue, persistentStorage).start();
}
}
Data.Builder dataBuilder = new Data.Builder().putInt(Job.KEY_RETRY_COUNT, jobParameters.getRetryCount())
.putLong(Job.KEY_RETRY_UNTIL, jobParameters.getRetryUntil())
.putLong(Job.KEY_SUBMIT_TIME, System.currentTimeMillis())
.putBoolean(Job.KEY_REQUIRES_MASTER_SECRET, jobParameters.requiresMasterSecret())
.putBoolean(Job.KEY_REQUIRES_SQLCIPHER, jobParameters.requiresSqlCipher());
Data data = job.serialize(dataBuilder);
/**
* @param context An Android {@link android.content.Context}.
* @return a {@link org.thoughtcrime.securesms.jobmanager.JobManager.Builder} used to construct a JobManager.
*/
public static Builder newBuilder(Context context) {
return new Builder(context);
}
OneTimeWorkRequest.Builder requestBuilder = new OneTimeWorkRequest.Builder(job.getClass())
.setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS);
public void setEncryptionKeys(EncryptionKeys keys) {
if (hasLoadedEncrypted.compareAndSet(false, true)) {
eventExecutor.execute(new LoadTask(keys));
}
}
if (jobParameters.requiresNetwork()) {
requestBuilder.setConstraints(NETWORK_CONSTRAINT);
}
/**
* Queue a {@link org.thoughtcrime.securesms.jobmanager.Job} to be executed.
*
* @param job The Job to be executed.
*/
public void add(final Job job) {
if (job.needsWakeLock()) {
job.setWakeLock(acquireWakeLock(context, job.toString(), job.getWakeLockTimeout()));
}
OneTimeWorkRequest request = requestBuilder.build();
eventExecutor.execute(new Runnable() {
@Override
public void run() {
try {
if (job.isPersistent()) {
persistentStorage.store(job);
}
job.onSubmit(request.getId());
dependencyInjector.injectDependencies(context, job);
job.onAdded();
jobQueue.add(job);
} catch (IOException e) {
Log.w("JobManager", e);
job.onCanceled();
}
String groupId = jobParameters.getGroupId();
if (groupId != null) {
ExistingWorkPolicy policy = jobParameters.shouldIgnoreDuplicates() ? ExistingWorkPolicy.KEEP : ExistingWorkPolicy.APPEND;
workManager.beginUniqueWork(groupId, policy, request).enqueue();
} else {
workManager.beginWith(request).enqueue();
}
});
}
@Override
public void onRequirementStatusChanged() {
eventExecutor.execute(new Runnable() {
@Override
public void run() {
jobQueue.onRequirementStatusChanged();
}
});
}
private PowerManager.WakeLock acquireWakeLock(Context context, String name, long timeout) {
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);
if (timeout == 0) wakeLock.acquire();
else wakeLock.acquire(timeout);
return wakeLock;
}
private class LoadTask implements Runnable {
private final EncryptionKeys keys;
public LoadTask(EncryptionKeys keys) {
this.keys = keys;
}
@Override
public void run() {
List<Job> pendingJobs;
if (keys == null) pendingJobs = persistentStorage.getAllUnencrypted();
else pendingJobs = persistentStorage.getAllEncrypted(keys);
jobQueue.addAll(pendingJobs);
}
}
public static class Builder {
private final Context context;
private String name;
private List<RequirementProvider> requirementProviders;
private DependencyInjector dependencyInjector;
private JobSerializer jobSerializer;
private int consumerThreads;
Builder(Context context) {
this.context = context;
this.consumerThreads = 5;
}
/**
* A name for the {@link org.thoughtcrime.securesms.jobmanager.JobManager}. This is a required parameter,
* and is linked to the durable queue used by persistent jobs.
*
* @param name The name for the JobManager to build.
* @return The builder.
*/
public Builder withName(String name) {
this.name = name;
return this;
}
/**
* The {@link org.thoughtcrime.securesms.jobmanager.requirements.RequirementProvider}s to register with this
* JobManager. Optional. Each {@link org.thoughtcrime.securesms.jobmanager.requirements.Requirement} an
* enqueued Job depends on should have a matching RequirementProvider registered here.
*
* @param requirementProviders The RequirementProviders
* @return The builder.
*/
public Builder withRequirementProviders(RequirementProvider... requirementProviders) {
this.requirementProviders = Arrays.asList(requirementProviders);
return this;
}
/**
* The {@link org.thoughtcrime.securesms.jobmanager.dependencies.DependencyInjector} to use for injecting
* dependencies into {@link Job}s. Optional. Injection occurs just before a Job's onAdded() callback, or
* after deserializing a persistent job.
*
* @param dependencyInjector The injector to use.
* @return The builder.
*/
public Builder withDependencyInjector(DependencyInjector dependencyInjector) {
this.dependencyInjector = dependencyInjector;
return this;
}
/**
* The {@link org.thoughtcrime.securesms.jobmanager.persistence.JobSerializer} to use for persistent Jobs.
* Required if persistent Jobs are used.
*
* @param jobSerializer The serializer to use.
* @return The builder.
*/
public Builder withJobSerializer(JobSerializer jobSerializer) {
this.jobSerializer = jobSerializer;
return this;
}
/**
* Set the number of threads dedicated to consuming Jobs from the queue and executing them.
*
* @param consumerThreads The number of threads.
* @return The builder.
*/
public Builder withConsumerThreads(int consumerThreads) {
this.consumerThreads = consumerThreads;
return this;
}
/**
* @return A constructed JobManager.
*/
public JobManager build() {
if (name == null) {
throw new IllegalArgumentException("You must specify a name!");
}
if (requirementProviders == null) {
requirementProviders = new LinkedList<>();
}
return new JobManager(context, name, requirementProviders,
dependencyInjector, jobSerializer,
consumerThreads);
}
}
}

View File

@@ -16,12 +16,16 @@
*/
package org.thoughtcrime.securesms.jobmanager;
import org.thoughtcrime.securesms.jobmanager.requirements.NetworkBackoffRequirement;
import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement;
import org.thoughtcrime.securesms.jobmanager.requirements.Requirement;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.jobs.requirements.NetworkOrServiceRequirement;
import org.thoughtcrime.securesms.jobs.requirements.SqlCipherMigrationRequirement;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* The set of parameters that describe a {@link org.thoughtcrime.securesms.jobmanager.Job}.
@@ -30,46 +34,86 @@ public class JobParameters implements Serializable {
private static final long serialVersionUID = 4880456378402584584L;
private transient EncryptionKeys encryptionKeys;
private final List<Requirement> requirements;
private final boolean isPersistent;
private final boolean requiresNetwork;
private final boolean requiresMasterSecret;
private final boolean requiresSqlCipher;
private final int retryCount;
private final long retryUntil;
private final String groupId;
private final boolean wakeLock;
private final long wakeLockTimeout;
private final boolean ignoreDuplicates;
private JobParameters(List<Requirement> requirements,
boolean isPersistent, String groupId,
EncryptionKeys encryptionKeys,
int retryCount, long retryUntil, boolean wakeLock,
long wakeLockTimeout)
private JobParameters(String groupId,
boolean ignoreDuplicates,
boolean requiresNetwork,
boolean requiresMasterSecret,
boolean requiresSqlCipher,
int retryCount,
long retryUntil)
{
this.requirements = requirements;
this.isPersistent = isPersistent;
this.groupId = groupId;
this.encryptionKeys = encryptionKeys;
this.retryCount = retryCount;
this.retryUntil = retryUntil;
this.wakeLock = wakeLock;
this.wakeLockTimeout = wakeLockTimeout;
this.groupId = groupId;
this.ignoreDuplicates = ignoreDuplicates;
this.requirements = Collections.emptyList();
this.requiresNetwork = requiresNetwork;
this.requiresMasterSecret = requiresMasterSecret;
this.requiresSqlCipher = requiresSqlCipher;
this.retryCount = retryCount;
this.retryUntil = retryUntil;
}
public List<Requirement> getRequirements() {
return requirements;
public boolean shouldIgnoreDuplicates() {
return ignoreDuplicates;
}
public boolean isPersistent() {
return isPersistent;
public boolean requiresNetwork() {
return requiresNetwork || hasNetworkRequirement(requirements);
}
public EncryptionKeys getEncryptionKeys() {
return encryptionKeys;
public boolean requiresMasterSecret() {
return requiresMasterSecret || hasMasterSecretRequirement(requirements);
}
public void setEncryptionKeys(EncryptionKeys encryptionKeys) {
this.encryptionKeys = encryptionKeys;
public boolean requiresSqlCipher() {
return requiresSqlCipher || hasSqlCipherRequirement(requirements);
}
private boolean hasNetworkRequirement(List<Requirement> requirements) {
if (requirements == null || requirements.size() == 0) return false;
for (Requirement requirement : requirements) {
if (requirement instanceof NetworkRequirement ||
requirement instanceof NetworkOrServiceRequirement ||
requirement instanceof NetworkBackoffRequirement)
{
return true;
}
}
return false;
}
private boolean hasMasterSecretRequirement(List<Requirement> requirements) {
if (requirements == null || requirements.size() == 0) return false;
for (Requirement requirement : requirements) {
if (requirement instanceof MasterSecretRequirement) {
return true;
}
}
return false;
}
private boolean hasSqlCipherRequirement(List<Requirement> requirements) {
if (requirements == null || requirements.size() == 0) return false;
for (Requirement requirement : requirements) {
if (requirement instanceof SqlCipherMigrationRequirement) {
return true;
}
}
return false;
}
public int getRetryCount() {
@@ -91,52 +135,29 @@ public class JobParameters implements Serializable {
return groupId;
}
public boolean needsWakeLock() {
return wakeLock;
}
public long getWakeLockTimeout() {
return wakeLockTimeout;
}
public static class Builder {
private List<Requirement> requirements = new LinkedList<>();
private boolean isPersistent = false;
private EncryptionKeys encryptionKeys = null;
private int retryCount = 100;
private long retryDuration = 0;
private String groupId = null;
private boolean wakeLock = false;
private long wakeLockTimeout = 0;
private int retryCount = 100;
private long retryDuration = 0;
private String groupId = null;
private boolean ignoreDuplicates = false;
private boolean requiresNetwork = false;
private boolean requiresSqlCipher = false;
private boolean requiresMasterSecret = false;
/**
* Specify a {@link org.thoughtcrime.securesms.jobmanager.requirements.Requirement }that must be met
* before the Job is executed. May be called multiple times to register multiple requirements.
* @param requirement The Requirement that must be met.
* @return the builder.
*/
public Builder withRequirement(Requirement requirement) {
this.requirements.add(requirement);
public Builder withNetworkRequirement() {
requiresNetwork = true;
return this;
}
/**
* Specify that the Job should be durably persisted to disk, so that it remains in the queue
* across application restarts.
* @return The builder.
*/
public Builder withPersistence() {
this.isPersistent = true;
@Deprecated
public Builder withMasterSecretRequirement() {
requiresMasterSecret = true;
return this;
}
/**
* Specify that the job should use encryption when durably persisted to disk.
* @param encryptionKeys The keys to encrypt the serialized job with before persisting.
* @return the builder.
*/
public Builder withEncryption(EncryptionKeys encryptionKeys) {
this.encryptionKeys = encryptionKeys;
@Deprecated
public Builder withSqlCipherRequirement() {
requiresSqlCipher = true;
return this;
}
@@ -153,6 +174,11 @@ public class JobParameters implements Serializable {
return this;
}
/**
* Specify for how long we should keep retrying this job. Ignored if retryCount is set.
* @param duration The duration (in ms) for how long we should keep retrying this job for.
* @return the builder
*/
public Builder withRetryDuration(long duration) {
this.retryDuration = duration;
this.retryCount = 0;
@@ -172,35 +198,25 @@ public class JobParameters implements Serializable {
}
/**
* Specify whether this job should hold a wake lock.
* If true, only one job with this groupId can be active at a time. If a job with the same
* groupId is already running, then subsequent jobs will be ignored silently. Only has an effect
* if a groupId has been specified via {@link #withGroupId(String)}.
* <p />
* Defaults to false.
*
* @param needsWakeLock If set, this job will acquire a wakelock on add(), and hold it until
* run() completes, or cancel().
* @param timeout Specify a timeout for the wakelock. A timeout of
* 0 will result in no timeout.
*
* @return the builder.
* @param ignoreDuplicates Whether to ignore duplicates.
* @return the builder
*/
public Builder withWakeLock(boolean needsWakeLock, long timeout, TimeUnit unit) {
this.wakeLock = needsWakeLock;
this.wakeLockTimeout = unit.toMillis(timeout);
public Builder withDuplicatesIgnored(boolean ignoreDuplicates) {
this.ignoreDuplicates = ignoreDuplicates;
return this;
}
/**
* Specify whether this job should hold a wake lock.
*
* @return the builder.
*/
public Builder withWakeLock(boolean needsWakeLock) {
return withWakeLock(needsWakeLock, 0, TimeUnit.MILLISECONDS);
}
/**
* @return the JobParameters instance that describes a Job.
*/
public JobParameters create() {
return new JobParameters(requirements, isPersistent, groupId, encryptionKeys, retryCount, System.currentTimeMillis() + retryDuration, wakeLock, wakeLockTimeout);
return new JobParameters(groupId, ignoreDuplicates, requiresNetwork, requiresMasterSecret, requiresSqlCipher, retryCount, System.currentTimeMillis() + retryDuration);
}
}
}

View File

@@ -1,118 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.jobmanager;
import android.support.annotation.NonNull;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
class JobQueue {
private final Map<String, Job> activeGroupIds = new HashMap<>();
private final LinkedList<Job> jobQueue = new LinkedList<>();
synchronized void onRequirementStatusChanged() {
notifyAll();
}
synchronized void add(Job job) {
jobQueue.add(job);
processJobAddition(job);
notifyAll();
}
synchronized void addAll(List<Job> jobs) {
jobQueue.addAll(jobs);
for (Job job : jobs) {
processJobAddition(job);
}
notifyAll();
}
private void processJobAddition(@NonNull Job job) {
if (isJobActive(job) && isGroupIdAvailable(job)) {
setGroupIdUnavailable(job);
} else if (!isGroupIdAvailable(job)) {
Job blockingJob = activeGroupIds.get(job.getGroupId());
blockingJob.resetRunStats();
}
}
synchronized void push(Job job) {
jobQueue.addFirst(job);
}
synchronized Job getNext() {
try {
Job nextAvailableJob;
while ((nextAvailableJob = getNextAvailableJob()) == null) {
wait();
}
return nextAvailableJob;
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
synchronized void setGroupIdAvailable(String groupId) {
if (groupId != null) {
activeGroupIds.remove(groupId);
notifyAll();
}
}
private Job getNextAvailableJob() {
if (jobQueue.isEmpty()) return null;
ListIterator<Job> iterator = jobQueue.listIterator();
while (iterator.hasNext()) {
Job job = iterator.next();
if (job.isRequirementsMet() && isGroupIdAvailable(job)) {
iterator.remove();
setGroupIdUnavailable(job);
return job;
}
}
return null;
}
private boolean isJobActive(@NonNull Job job) {
return job.getRetryUntil() > 0 && job.getRunIteration() > 0;
}
private boolean isGroupIdAvailable(@NonNull Job requester) {
String groupId = requester.getGroupId();
return groupId == null || !activeGroupIds.containsKey(groupId) || activeGroupIds.get(groupId).equals(requester);
}
private void setGroupIdUnavailable(@NonNull Job job) {
String groupId = job.getGroupId();
if (groupId != null) {
activeGroupIds.put(groupId, job);
}
}
}

View File

@@ -0,0 +1,80 @@
package org.thoughtcrime.securesms.jobmanager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.work.Data;
/**
* A wrapper around {@link Data} that does its best to throw an exception whenever a key isn't
* present in the {@link Data} object.
*/
public class SafeData {
private final static int INVALID_INT = Integer.MIN_VALUE;
private final static long INVALID_LONG = Long.MIN_VALUE;
private final Data data;
public SafeData(@NonNull Data data) {
this.data = data;
}
public int getInt(@NonNull String key) {
int value = data.getInt(key, INVALID_INT);
if (value == INVALID_INT) {
throw new IllegalStateException("Missing key: " + key);
}
return value;
}
public long getLong(@NonNull String key) {
long value = data.getLong(key, INVALID_LONG);
if (value == INVALID_LONG) {
throw new IllegalStateException("Missing key: " + key);
}
return value;
}
public @NonNull String getString(@NonNull String key) {
String value = data.getString(key);
if (value == null) {
throw new IllegalStateException("Missing key: " + key);
}
return value;
}
public @Nullable String getNullableString(@NonNull String key) {
return data.getString(key);
}
public @NonNull String[] getStringArray(@NonNull String key) {
String[] value = data.getStringArray(key);
if (value == null) {
throw new IllegalStateException("Missing key: " + key);
}
return value;
}
public @NonNull long[] getLongArray(@NonNull String key) {
long[] value = data.getLongArray(key);
if (value == null) {
throw new IllegalStateException("Missing key: " + key);
}
return value;
}
public boolean getBoolean(@NonNull String key, boolean defaultValue) {
return data.getBoolean(key, defaultValue);
}
}

View File

@@ -1,36 +0,0 @@
package org.thoughtcrime.securesms.jobmanager.dependencies;
import android.content.Context;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.requirements.Requirement;
public class AggregateDependencyInjector {
private final DependencyInjector dependencyInjector;
public AggregateDependencyInjector(DependencyInjector dependencyInjector) {
this.dependencyInjector = dependencyInjector;
}
public void injectDependencies(Context context, Job job) {
if (job instanceof ContextDependent) {
((ContextDependent)job).setContext(context);
}
for (Requirement requirement : job.getRequirements()) {
if (requirement instanceof ContextDependent) {
((ContextDependent)requirement).setContext(context);
}
}
if (dependencyInjector != null) {
dependencyInjector.injectDependencies(job);
for (Requirement requirement : job.getRequirements()) {
dependencyInjector.injectDependencies(requirement);
}
}
}
}

View File

@@ -16,7 +16,6 @@
*/
package org.thoughtcrime.securesms.jobmanager.persistence;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
@@ -24,7 +23,6 @@ import android.database.sqlite.SQLiteOpenHelper;
import org.thoughtcrime.securesms.jobmanager.EncryptionKeys;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.dependencies.AggregateDependencyInjector;
import org.thoughtcrime.securesms.logging.Log;
import java.io.IOException;
@@ -43,38 +41,18 @@ public class PersistentStorage {
private static final String DATABASE_CREATE = String.format("CREATE TABLE %s (%s INTEGER PRIMARY KEY, %s TEXT NOT NULL, %s INTEGER DEFAULT 0);",
TABLE_NAME, ID, ITEM, ENCRYPTED);
private final Context context;
private final DatabaseHelper databaseHelper;
private final JobSerializer jobSerializer;
private final AggregateDependencyInjector dependencyInjector;
private final DatabaseHelper databaseHelper;
private final JobSerializer jobSerializer;
public PersistentStorage(Context context, String name,
JobSerializer serializer,
AggregateDependencyInjector dependencyInjector)
{
public PersistentStorage(Context context, String name, JobSerializer serializer) {
this.databaseHelper = new DatabaseHelper(context, "_jobqueue-" + name);
this.context = context;
this.jobSerializer = serializer;
this.dependencyInjector = dependencyInjector;
}
public void store(Job job) throws IOException {
ContentValues contentValues = new ContentValues();
contentValues.put(ITEM, jobSerializer.serialize(job));
contentValues.put(ENCRYPTED, job.getEncryptionKeys() != null);
long id = databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
job.setPersistentId(id);
}
public List<Job> getAllUnencrypted() {
return getJobs(null, ENCRYPTED + " = 0");
}
public List<Job> getAllEncrypted(EncryptionKeys keys) {
return getJobs(keys, ENCRYPTED + " = 1");
}
private List<Job> getJobs(EncryptionKeys keys, String where) {
List<Job> results = new LinkedList<>();
SQLiteDatabase database = databaseHelper.getReadableDatabase();
@@ -90,11 +68,6 @@ public class PersistentStorage {
try{
Job job = jobSerializer.deserialize(keys, encrypted, item);
job.setPersistentId(id);
job.setEncryptionKeys(keys);
dependencyInjector.injectDependencies(context, job);
results.add(job);
} catch (IOException e) {
Log.w("PersistentStore", e);

View File

@@ -1,35 +0,0 @@
package org.thoughtcrime.securesms.jobmanager.requirements;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.logging.Log;
import java.util.UUID;
public class BackoffReceiver extends BroadcastReceiver {
private static final String TAG = BackoffReceiver.class.getSimpleName();
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "Received an alarm to retry a job with ID: " + intent.getAction());
ApplicationContext.getInstance(context).getJobManager().onRequirementStatusChanged();
}
public static void setUniqueAlarm(@NonNull Context context, long time) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, BackoffReceiver.class);
intent.setAction(BuildConfig.APPLICATION_ID + UUID.randomUUID().toString());
alarmManager.set(AlarmManager.RTC_WAKEUP, time, PendingIntent.getBroadcast(context, 0, intent, 0));
Log.i(TAG, "Set an alarm to retry a job in " + (time - System.currentTimeMillis()) + " ms with ID: " + intent.getAction());
}
}

View File

@@ -31,15 +31,6 @@ public class NetworkBackoffRequirement implements Requirement, ContextDependent
@Override
public void onRetry(@NonNull Job job) {
Log.i(TAG, "onRetry()");
if (!(new NetworkRequirement(context).isPresent())) {
Log.i(TAG, "No network. Resetting run stats.");
job.resetRunStats();
return;
}
BackoffReceiver.setUniqueAlarm(context, NetworkBackoffRequirement.calculateNextRunTime(job));
}
@Override
@@ -48,9 +39,6 @@ public class NetworkBackoffRequirement implements Requirement, ContextDependent
}
private static long calculateNextRunTime(@NonNull Job job) {
long targetTime = job.getLastRunTime() + (long) (Math.pow(2, job.getRunIteration() - 1) * 1000);
long furthestTime = System.currentTimeMillis() + MAX_WAIT;
return Math.min(targetTime, Math.min(furthestTime, job.getRetryUntil()));
return 0;
}
}