session-android/src/org/thoughtcrime/securesms/jobmanager/Job.java

271 lines
8.4 KiB
Java
Raw Normal View History

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());
}
boolean foregroundRunning = false;
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;
}
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)) {
2018-10-01 23:14:45 +00:00
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) {
2018-10-01 23:14:45 +00:00
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) {
2018-10-01 23:14:45 +00:00
log(message, null);
}
2018-10-01 23:14:45 +00:00
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() + ")";
}
}