2018-06-18 19:27:04 +00:00
|
|
|
package org.thoughtcrime.securesms.jobmanager;
|
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
import android.content.Context;
|
|
|
|
import android.support.annotation.NonNull;
|
|
|
|
import android.support.annotation.Nullable;
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
import org.thoughtcrime.securesms.ApplicationContext;
|
2018-10-03 16:42:35 +00:00
|
|
|
import org.thoughtcrime.securesms.R;
|
2018-08-09 14:15:43 +00:00
|
|
|
import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent;
|
2018-10-03 16:42:35 +00:00
|
|
|
import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement;
|
2018-08-09 14:15:43 +00:00
|
|
|
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
|
|
|
|
import org.thoughtcrime.securesms.jobs.requirements.SqlCipherMigrationRequirement;
|
|
|
|
import org.thoughtcrime.securesms.logging.Log;
|
2018-10-03 16:42:35 +00:00
|
|
|
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
2018-06-18 19:27:04 +00:00
|
|
|
|
|
|
|
import java.io.Serializable;
|
2018-10-29 05:13:51 +00:00
|
|
|
import java.util.HashMap;
|
|
|
|
import java.util.Map;
|
2018-08-09 14:15:43 +00:00
|
|
|
import java.util.UUID;
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
import androidx.work.Data;
|
|
|
|
import androidx.work.Worker;
|
|
|
|
import androidx.work.WorkerParameters;
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
public abstract class Job extends Worker implements Serializable {
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
private static final long serialVersionUID = -4658540468214421276L;
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
private static final String TAG = Job.class.getSimpleName();
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-10-29 05:13:51 +00:00
|
|
|
private static final WorkLockManager WORK_LOCK_MANAGER = new WorkLockManager();
|
|
|
|
|
2018-11-15 20:05:08 +00:00
|
|
|
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_SQLCIPHER = "Job_requires_sqlcipher";
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
private JobParameters parameters;
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
public Job(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
|
|
|
super(context, workerParams);
|
2018-06-18 19:27:04 +00:00
|
|
|
}
|
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
/**
|
|
|
|
* Invoked when a job is first created in our own codebase.
|
|
|
|
*/
|
|
|
|
protected Job(@Nullable JobParameters parameters) {
|
|
|
|
this.parameters = parameters;
|
2018-06-20 02:22:39 +00:00
|
|
|
}
|
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
@Override
|
2018-10-29 05:13:51 +00:00
|
|
|
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() {
|
2018-08-09 14:15:43 +00:00
|
|
|
Data data = getInputData();
|
2018-06-20 02:22:39 +00:00
|
|
|
|
2018-10-29 05:13:51 +00:00
|
|
|
log("doWorkInternal()" + logSuffix());
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
ApplicationContext.getInstance(getApplicationContext()).injectDependencies(this);
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
if (this instanceof ContextDependent) {
|
|
|
|
((ContextDependent)this).setContext(getApplicationContext());
|
|
|
|
}
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-10-03 16:42:35 +00:00
|
|
|
boolean foregroundRunning = false;
|
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
try {
|
2018-11-17 17:56:52 +00:00
|
|
|
initialize(new SafeData(data));
|
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
if (withinRetryLimits(data)) {
|
|
|
|
if (requirementsMet(data)) {
|
2018-10-03 16:42:35 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
onRun();
|
2018-10-03 16:42:35 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
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)) {
|
2018-10-01 23:14:45 +00:00
|
|
|
log("Retrying after a retryable exception." + logSuffix(), e);
|
2018-08-09 14:15:43 +00:00
|
|
|
return retry();
|
|
|
|
}
|
|
|
|
warn("Failing due to an exception." + logSuffix(), e);
|
|
|
|
return cancel();
|
2018-10-03 16:42:35 +00:00
|
|
|
} finally {
|
|
|
|
if (foregroundRunning) {
|
|
|
|
Log.i(TAG, "Stopping the foreground service." + logSuffix());
|
|
|
|
GenericForegroundService.stopForegroundTask(getApplicationContext());
|
|
|
|
}
|
2018-08-09 14:15:43 +00:00
|
|
|
}
|
2018-06-18 19:27:04 +00:00
|
|
|
}
|
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
@Override
|
|
|
|
public void onStopped(boolean cancelled) {
|
|
|
|
if (cancelled) {
|
|
|
|
warn("onStopped() with cancellation signal." + logSuffix());
|
|
|
|
onCanceled();
|
2018-10-29 05:13:51 +00:00
|
|
|
} else {
|
|
|
|
log("onStopped()" + logSuffix());
|
2018-08-09 14:15:43 +00:00
|
|
|
}
|
2018-06-18 19:27:04 +00:00
|
|
|
}
|
|
|
|
|
2018-10-21 05:52:14 +00:00
|
|
|
final void onSubmit(@NonNull Context context, @NonNull UUID id) {
|
2018-10-01 23:14:45 +00:00
|
|
|
Log.i(TAG, buildLog(id, "onSubmit()"));
|
2018-10-21 05:52:14 +00:00
|
|
|
|
|
|
|
if (this instanceof ContextDependent) {
|
|
|
|
((ContextDependent) this).setContext(context);
|
|
|
|
}
|
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
onAdded();
|
2018-06-18 19:27:04 +00:00
|
|
|
}
|
|
|
|
|
2018-10-03 16:42:35 +00:00
|
|
|
/**
|
|
|
|
* @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);
|
|
|
|
}
|
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
/**
|
|
|
|
* Called after a run has finished and we've determined a retry is required, but before the next
|
|
|
|
* attempt is run.
|
|
|
|
*/
|
|
|
|
protected void onRetry() { }
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
/**
|
|
|
|
* 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() { }
|
2018-06-20 02:22:39 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
2018-06-20 02:22:39 +00:00
|
|
|
|
2018-06-18 19:27:04 +00:00
|
|
|
/**
|
2018-08-09 14:15:43 +00:00
|
|
|
* 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.
|
2018-06-18 19:27:04 +00:00
|
|
|
*/
|
2018-08-09 14:15:43 +00:00
|
|
|
protected abstract void initialize(@NonNull SafeData data);
|
2018-06-18 19:27:04 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Called to actually execute the job.
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
2018-08-09 14:15:43 +00:00
|
|
|
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();
|
2018-06-18 19:27:04 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2018-08-09 14:15:43 +00:00
|
|
|
protected abstract boolean onShouldRetry(Exception exception);
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
@Nullable JobParameters getJobParameters() {
|
|
|
|
return parameters;
|
|
|
|
}
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
private Result retry() {
|
|
|
|
onRetry();
|
|
|
|
return Result.RETRY;
|
|
|
|
}
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
private Result cancel() {
|
|
|
|
onCanceled();
|
|
|
|
return Result.SUCCESS;
|
|
|
|
}
|
2018-06-18 19:27:04 +00:00
|
|
|
|
2018-10-03 16:42:35 +00:00
|
|
|
private boolean requirementsMet(@NonNull Data data) {
|
2018-08-09 14:15:43 +00:00
|
|
|
boolean met = true;
|
|
|
|
|
|
|
|
if (data.getBoolean(KEY_REQUIRES_SQLCIPHER, false)) {
|
|
|
|
met &= new SqlCipherMigrationRequirement(getApplicationContext()).isPresent();
|
|
|
|
}
|
|
|
|
|
|
|
|
return met;
|
|
|
|
}
|
|
|
|
|
2018-10-03 16:42:35 +00:00
|
|
|
private boolean withinRetryLimits(@NonNull Data data) {
|
2018-08-09 14:15:43 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-10-03 16:42:35 +00:00
|
|
|
private boolean needsForegroundService(@NonNull Data data) {
|
|
|
|
NetworkRequirement networkRequirement = new NetworkRequirement(getApplicationContext());
|
|
|
|
boolean requiresNetwork = data.getBoolean(KEY_REQUIRES_NETWORK, false);
|
|
|
|
|
|
|
|
return requiresNetwork && !networkRequirement.isPresent();
|
|
|
|
}
|
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
private void log(@NonNull String message) {
|
2018-10-01 23:14:45 +00:00
|
|
|
log(message, null);
|
2018-08-09 14:15:43 +00:00
|
|
|
}
|
|
|
|
|
2018-10-01 23:14:45 +00:00
|
|
|
private void log(@NonNull String message, @Nullable Throwable t) {
|
|
|
|
Log.i(TAG, buildLog(getId(), message), t);
|
2018-08-09 14:15:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2018-10-29 05:13:51 +00:00
|
|
|
return " (Time since submission: " + timeSinceSubmission + " ms, Run attempt: " + getRunAttemptCount() + ", isStopped: " + isStopped() + ")";
|
2018-08-09 14:15:43 +00:00
|
|
|
}
|
2018-10-29 05:13:51 +00:00
|
|
|
|
2018-06-18 19:27:04 +00:00
|
|
|
}
|