There are rare corner cases where a Job could be preempted by the JobScheduler and then be rescheduled before the preempted job finished running. This could create weird race conditions with bad consequences. To fix this, we create a fun locking system that prevents two jobs with the same UUID from running at the same time.
270 lines
8.4 KiB
Java
270 lines
8.4 KiB
Java
package org.thoughtcrime.securesms.jobmanager;
|
|
|
|
import android.content.Context;
|
|
import android.support.annotation.NonNull;
|
|
import android.support.annotation.Nullable;
|
|
|
|
import org.thoughtcrime.securesms.ApplicationContext;
|
|
import org.thoughtcrime.securesms.R;
|
|
import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent;
|
|
import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement;
|
|
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
|
|
import org.thoughtcrime.securesms.jobs.requirements.SqlCipherMigrationRequirement;
|
|
import org.thoughtcrime.securesms.logging.Log;
|
|
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
|
|
|
import java.io.Serializable;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.UUID;
|
|
|
|
import androidx.work.Data;
|
|
import androidx.work.Worker;
|
|
import androidx.work.WorkerParameters;
|
|
|
|
public abstract class Job extends Worker implements Serializable {
|
|
|
|
private static final long serialVersionUID = -4658540468214421276L;
|
|
|
|
private static final String TAG = Job.class.getSimpleName();
|
|
|
|
private static final WorkLockManager WORK_LOCK_MANAGER = new WorkLockManager();
|
|
|
|
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_NETWORK = "Job_requires_network";
|
|
static final String KEY_REQUIRES_MASTER_SECRET = "Job_requires_master_secret";
|
|
static final String KEY_REQUIRES_SQLCIPHER = "Job_requires_sqlcipher";
|
|
|
|
private JobParameters parameters;
|
|
|
|
public Job(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
|
super(context, workerParams);
|
|
}
|
|
|
|
/**
|
|
* Invoked when a job is first created in our own codebase.
|
|
*/
|
|
protected Job(@Nullable JobParameters parameters) {
|
|
this.parameters = parameters;
|
|
}
|
|
|
|
@Override
|
|
public @NonNull Result doWork() {
|
|
log("doWork()" + logSuffix());
|
|
|
|
try (WorkLockManager.WorkLock workLock = WORK_LOCK_MANAGER.acquire(getId())) {
|
|
Result result = workLock.getResult();
|
|
|
|
if (result == null) {
|
|
result = doWorkInternal();
|
|
workLock.setResult(result);
|
|
} else {
|
|
log("Using result from preempted run (" + result + ")." + logSuffix());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private @NonNull Result doWorkInternal() {
|
|
Data data = getInputData();
|
|
|
|
log("doWorkInternal()" + logSuffix());
|
|
|
|
ApplicationContext.getInstance(getApplicationContext()).injectDependencies(this);
|
|
|
|
if (this instanceof ContextDependent) {
|
|
((ContextDependent)this).setContext(getApplicationContext());
|
|
}
|
|
|
|
initialize(new SafeData(data));
|
|
|
|
boolean foregroundRunning = false;
|
|
|
|
try {
|
|
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;
|
|
}
|
|
|
|
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(), e);
|
|
return retry();
|
|
}
|
|
warn("Failing due to an exception." + logSuffix(), e);
|
|
return cancel();
|
|
} finally {
|
|
if (foregroundRunning) {
|
|
Log.i(TAG, "Stopping the foreground service." + logSuffix());
|
|
GenericForegroundService.stopForegroundTask(getApplicationContext());
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onStopped(boolean cancelled) {
|
|
if (cancelled) {
|
|
warn("onStopped() with cancellation signal." + logSuffix());
|
|
onCanceled();
|
|
} else {
|
|
log("onStopped()" + logSuffix());
|
|
}
|
|
}
|
|
|
|
final void onSubmit(@NonNull Context context, @NonNull UUID id) {
|
|
Log.i(TAG, buildLog(id, "onSubmit()"));
|
|
|
|
if (this instanceof ContextDependent) {
|
|
((ContextDependent) this).setContext(context);
|
|
}
|
|
|
|
onAdded();
|
|
}
|
|
|
|
/**
|
|
* @return A string that represents what the task does. Will be shown in a foreground notification
|
|
* if necessary.
|
|
*/
|
|
protected String getDescription() {
|
|
return getApplicationContext().getString(R.string.Job_working_in_the_background);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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
|
|
* job should be retried.
|
|
*
|
|
* @param exception The exception onRun() threw.
|
|
* @return true if onRun() should be called again, false otherwise.
|
|
*/
|
|
protected abstract boolean onShouldRetry(Exception exception);
|
|
|
|
@Nullable JobParameters getJobParameters() {
|
|
return parameters;
|
|
}
|
|
|
|
private Result retry() {
|
|
onRetry();
|
|
return Result.RETRY;
|
|
}
|
|
|
|
private Result cancel() {
|
|
onCanceled();
|
|
return Result.SUCCESS;
|
|
}
|
|
|
|
private boolean requirementsMet(@NonNull 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(@NonNull 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 boolean needsForegroundService(@NonNull Data data) {
|
|
NetworkRequirement networkRequirement = new NetworkRequirement(getApplicationContext());
|
|
boolean requiresNetwork = data.getBoolean(KEY_REQUIRES_NETWORK, false);
|
|
|
|
return requiresNetwork && !networkRequirement.isPresent();
|
|
}
|
|
|
|
private void log(@NonNull String message) {
|
|
log(message, null);
|
|
}
|
|
|
|
private void log(@NonNull String message, @Nullable Throwable t) {
|
|
Log.i(TAG, buildLog(getId(), message), t);
|
|
}
|
|
|
|
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() + ", isStopped: " + isStopped() + ")";
|
|
}
|
|
|
|
}
|