diff --git a/src/org/thoughtcrime/securesms/jobmanager/ChainParameters.java b/src/org/thoughtcrime/securesms/jobmanager/ChainParameters.java new file mode 100644 index 0000000000..03fd006389 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/ChainParameters.java @@ -0,0 +1,45 @@ +package org.thoughtcrime.securesms.jobmanager; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.whispersystems.libsignal.util.guava.Optional; + +public class ChainParameters { + + private final String groupId; + private final boolean ignoreDuplicates; + + private ChainParameters(@NonNull String groupId, boolean ignoreDuplicates) { + this.groupId = groupId; + this.ignoreDuplicates = ignoreDuplicates; + } + + public Optional getGroupId() { + return Optional.fromNullable(groupId); + } + + public boolean shouldIgnoreDuplicates() { + return ignoreDuplicates; + } + + public static class Builder { + + private String groupId; + private boolean ignoreDuplicates; + + public Builder setGroupId(@Nullable String groupId) { + this.groupId = groupId; + return this; + } + + public Builder ignoreDuplicates(boolean ignore) { + this.ignoreDuplicates = ignore; + return this; + } + + public ChainParameters build() { + return new ChainParameters(groupId, ignoreDuplicates); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/Job.java b/src/org/thoughtcrime/securesms/jobmanager/Job.java index ab0642d11f..e9a98e32b2 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/Job.java +++ b/src/org/thoughtcrime/securesms/jobmanager/Job.java @@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement; import org.thoughtcrime.securesms.jobs.requirements.SqlCipherMigrationRequirement; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.service.GenericForegroundService; +import org.whispersystems.libsignal.util.guava.Optional; import java.io.Serializable; import java.util.Collections; @@ -33,6 +34,7 @@ public abstract class Job extends Worker implements Serializable { 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_FAILED = "Job_failed"; static final String KEY_REQUIRES_NETWORK = "Job_requires_network"; static final String KEY_REQUIRES_SQLCIPHER = "Job_requires_sqlcipher"; @@ -86,26 +88,31 @@ public abstract class Job extends Worker implements Serializable { try { initialize(new SafeData(data)); - if (withinRetryLimits(data)) { - if (requirementsMet(data)) { - if (needsForegroundService(data)) { - Log.i(TAG, "Running a foreground service with description '" + getDescription() + "' to aid in job execution." + logSuffix()); - GenericForegroundService.startForegroundTask(getApplicationContext(), getDescription()); - foregroundRunning = true; - } + if (data.getBoolean(KEY_FAILED, false)) { + warn("Failing due to a failure earlier in the chain." + logSuffix()); + return cancel(); + } - onRun(); - - log("Successfully completed." + logSuffix()); - return Result.success(); - } else { - log("Retrying due to unmet requirements." + logSuffix()); - return retry(); - } - } else { + if (!withinRetryLimits(data)) { warn("Failing after hitting the retry limit." + logSuffix()); return cancel(); } + + if (!requirementsMet(data)) { + log("Retrying due to unmet requirements." + logSuffix()); + return retry(); + } + + if (needsForegroundService(data)) { + Log.i(TAG, "Running a foreground service with description '" + getDescription() + "' to aid in job execution." + logSuffix()); + GenericForegroundService.startForegroundTask(getApplicationContext(), getDescription()); + foregroundRunning = true; + } + + onRun(); + + log("Successfully completed." + logSuffix()); + return success(); } catch (Exception e) { if (onShouldRetry(e)) { log("Retrying after a retryable exception." + logSuffix(), e); @@ -197,6 +204,10 @@ public abstract class Job extends Worker implements Serializable { return parameters; } + private Result success() { + return Result.success(); + } + private Result retry() { onRetry(); return Result.retry(); @@ -204,7 +215,7 @@ public abstract class Job extends Worker implements Serializable { private Result cancel() { onCanceled(); - return Result.success(); + return Result.success(new Data.Builder().putBoolean(KEY_FAILED, true).build()); } private boolean requirementsMet(@NonNull Data data) { diff --git a/src/org/thoughtcrime/securesms/jobmanager/JobManager.java b/src/org/thoughtcrime/securesms/jobmanager/JobManager.java index 4cc3dda281..20f962bfb4 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/JobManager.java +++ b/src/org/thoughtcrime/securesms/jobmanager/JobManager.java @@ -3,8 +3,14 @@ package org.thoughtcrime.securesms.jobmanager; import android.content.Context; import android.support.annotation.NonNull; +import com.annimon.stream.Stream; + import org.thoughtcrime.securesms.logging.Log; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -16,6 +22,7 @@ import androidx.work.Data; import androidx.work.ExistingWorkPolicy; import androidx.work.NetworkType; import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkContinuation; import androidx.work.WorkManager; public class JobManager { @@ -36,7 +43,25 @@ public class JobManager { this.workManager = workManager; } + public Chain startChain(@NonNull Job job) { + return startChain(Collections.singletonList(job)); + } + + public Chain startChain(@NonNull List jobs) { + return new Chain(jobs); + } + public void add(Job job) { + JobParameters jobParameters = job.getJobParameters(); + + if (jobParameters == null) { + throw new IllegalStateException("Jobs must have JobParameters at this stage. (" + job.getClass().getSimpleName() + ")"); + } + + startChain(job).enqueue(jobParameters.getSoloChainParameters()); + } + + private void enqueueChain(@NonNull Chain chain, @NonNull ChainParameters chainParameters) { executor.execute(() -> { try { workManager.pruneWork().getResult().get(); @@ -44,38 +69,85 @@ public class JobManager { Log.w(TAG, "Failed to prune work.", e); } - JobParameters jobParameters = job.getJobParameters(); + List> jobListChain = chain.getJobListChain(); + List> requestListChain = Stream.of(jobListChain).map(jl -> Stream.of(jl).map(this::toWorkRequest).toList()).toList(); - if (jobParameters == null) { - throw new IllegalStateException("Jobs must have JobParameters at this stage. (" + job.getClass().getSimpleName() + ")"); + if (jobListChain.isEmpty()) { + throw new IllegalStateException("Enqueued an empty chain."); } - 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_NETWORK, jobParameters.requiresNetwork()) - .putBoolean(Job.KEY_REQUIRES_SQLCIPHER, jobParameters.requiresSqlCipher()); - Data data = job.serialize(dataBuilder); - - OneTimeWorkRequest.Builder requestBuilder = new OneTimeWorkRequest.Builder(job.getClass()) - .setInputData(data) - .setBackoffCriteria(BackoffPolicy.LINEAR, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS); - - if (jobParameters.requiresNetwork()) { - requestBuilder.setConstraints(NETWORK_CONSTRAINT); + for (int i = 0; i < jobListChain.size(); i++) { + for (int j = 0; j < jobListChain.get(i).size(); j++) { + jobListChain.get(i).get(j).onSubmit(context, requestListChain.get(i).get(j).getId()); + } } - OneTimeWorkRequest request = requestBuilder.build(); + WorkContinuation continuation; - job.onSubmit(context, request.getId()); - - String groupId = jobParameters.getGroupId(); - if (groupId != null) { - ExistingWorkPolicy policy = jobParameters.shouldIgnoreDuplicates() ? ExistingWorkPolicy.KEEP : ExistingWorkPolicy.APPEND; - workManager.beginUniqueWork(groupId, policy, request).enqueue(); + if (chainParameters.getGroupId().isPresent()) { + ExistingWorkPolicy policy = chainParameters.shouldIgnoreDuplicates() ? ExistingWorkPolicy.KEEP : ExistingWorkPolicy.APPEND; + continuation = workManager.beginUniqueWork(chainParameters.getGroupId().get(), policy, requestListChain.get(0)); } else { - workManager.beginWith(request).enqueue(); + continuation = workManager.beginWith(requestListChain.get(0)); } + + for (int i = 1; i < requestListChain.size(); i++) { + continuation = continuation.then(requestListChain.get(i)); + } + + continuation.enqueue(); }); + + } + + private OneTimeWorkRequest toWorkRequest(@NonNull Job job) { + JobParameters jobParameters = job.getJobParameters(); + + if (jobParameters == null) { + throw new IllegalStateException("Jobs must have JobParameters at this stage. (" + job.getClass().getSimpleName() + ")"); + } + + 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_NETWORK, jobParameters.requiresNetwork()) + .putBoolean(Job.KEY_REQUIRES_SQLCIPHER, jobParameters.requiresSqlCipher()); + Data data = job.serialize(dataBuilder); + + OneTimeWorkRequest.Builder requestBuilder = new OneTimeWorkRequest.Builder(job.getClass()) + .setInputData(data) + .setBackoffCriteria(BackoffPolicy.LINEAR, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS); + + if (jobParameters.requiresNetwork()) { + requestBuilder.setConstraints(NETWORK_CONSTRAINT); + } + + return requestBuilder.build(); + } + + public class Chain { + + private final List> jobs = new LinkedList<>(); + + private Chain(@NonNull List jobs) { + this.jobs.add(new ArrayList<>(jobs)); + } + + public Chain then(@NonNull Job job) { + return then(Collections.singletonList(job)); + } + + public Chain then(@NonNull List jobs) { + this.jobs.add(new ArrayList<>(jobs)); + return this; + } + + public void enqueue(@NonNull ChainParameters chainParameters) { + enqueueChain(this, chainParameters); + } + + private List> getJobListChain() { + return jobs; + } } } diff --git a/src/org/thoughtcrime/securesms/jobmanager/JobParameters.java b/src/org/thoughtcrime/securesms/jobmanager/JobParameters.java index 39af21db7b..f45a9b50e3 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/JobParameters.java +++ b/src/org/thoughtcrime/securesms/jobmanager/JobParameters.java @@ -104,6 +104,13 @@ public class JobParameters implements Serializable { return retryUntil; } + public ChainParameters getSoloChainParameters() { + return new ChainParameters.Builder() + .setGroupId(groupId) + .ignoreDuplicates(ignoreDuplicates) + .build(); + } + /** * @return a builder used to construct JobParameters. */