mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 08:09:12 +01:00
Move all files to natural position.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.Application;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Schedules tasks using the {@link AlarmManager}.
|
||||
*
|
||||
* Given that this scheduler is only used when {@link KeepAliveService} is also used (which keeps
|
||||
* all of the {@link ConstraintObserver}s running), this only needs to schedule future runs in
|
||||
* situations where all constraints are already met. Otherwise, the {@link ConstraintObserver}s will
|
||||
* trigger future runs when the constraints are met.
|
||||
*
|
||||
* For the same reason, this class also doesn't have to schedule jobs that don't have delays.
|
||||
*
|
||||
* Important: Only use on API < 26.
|
||||
*/
|
||||
public class AlarmManagerScheduler implements Scheduler {
|
||||
|
||||
private static final String TAG = AlarmManagerScheduler.class.getSimpleName();
|
||||
|
||||
private final Application application;
|
||||
|
||||
AlarmManagerScheduler(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void schedule(long delay, @NonNull List<Constraint> constraints) {
|
||||
if (delay > 0 && Stream.of(constraints).allMatch(Constraint::isMet)) {
|
||||
setUniqueAlarm(application, System.currentTimeMillis() + delay);
|
||||
}
|
||||
}
|
||||
|
||||
private void setUniqueAlarm(@NonNull Context context, long time) {
|
||||
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
Intent intent = new Intent(context, RetryReceiver.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.");
|
||||
}
|
||||
|
||||
public static class RetryReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.i(TAG, "Received an alarm to retry a job.");
|
||||
ApplicationDependencies.getJobManager().wakeUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
public class BootReceiver extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = BootReceiver.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.i(TAG, "Boot received. Application is created, kickstarting JobManager.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
class CompositeScheduler implements Scheduler {
|
||||
|
||||
private final List<Scheduler> schedulers;
|
||||
|
||||
CompositeScheduler(@NonNull Scheduler... schedulers) {
|
||||
this.schedulers = Arrays.asList(schedulers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void schedule(long delay, @NonNull List<Constraint> constraints) {
|
||||
for (Scheduler scheduler : schedulers) {
|
||||
scheduler.schedule(delay, constraints);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.app.job.JobInfo;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public interface Constraint {
|
||||
|
||||
boolean isMet();
|
||||
|
||||
@NonNull String getFactoryKey();
|
||||
|
||||
@RequiresApi(26)
|
||||
void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder);
|
||||
|
||||
interface Factory<T extends Constraint> {
|
||||
T create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ConstraintInstantiator {
|
||||
|
||||
private final Map<String, Constraint.Factory> constraintFactories;
|
||||
|
||||
ConstraintInstantiator(@NonNull Map<String, Constraint.Factory> constraintFactories) {
|
||||
this.constraintFactories = new HashMap<>(constraintFactories);
|
||||
}
|
||||
|
||||
public @NonNull Constraint instantiate(@NonNull String constraintFactoryKey) {
|
||||
if (constraintFactories.containsKey(constraintFactoryKey)) {
|
||||
return constraintFactories.get(constraintFactoryKey).create();
|
||||
} else {
|
||||
throw new IllegalStateException("Tried to instantiate a constraint with key '" + constraintFactoryKey + "', but no matching factory was found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface ConstraintObserver {
|
||||
|
||||
void register(@NonNull Notifier notifier);
|
||||
|
||||
interface Notifier {
|
||||
void onConstraintMet(@NonNull String reason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Data {
|
||||
|
||||
public static final Data EMPTY = new Data.Builder().build();
|
||||
|
||||
@JsonProperty private final Map<String, String> strings;
|
||||
@JsonProperty private final Map<String, String[]> stringArrays;
|
||||
@JsonProperty private final Map<String, Integer> integers;
|
||||
@JsonProperty private final Map<String, int[]> integerArrays;
|
||||
@JsonProperty private final Map<String, Long> longs;
|
||||
@JsonProperty private final Map<String, long[]> longArrays;
|
||||
@JsonProperty private final Map<String, Float> floats;
|
||||
@JsonProperty private final Map<String, float[]> floatArrays;
|
||||
@JsonProperty private final Map<String, Double> doubles;
|
||||
@JsonProperty private final Map<String, double[]> doubleArrays;
|
||||
@JsonProperty private final Map<String, Boolean> booleans;
|
||||
@JsonProperty private final Map<String, boolean[]> booleanArrays;
|
||||
|
||||
public Data(@JsonProperty("strings") @NonNull Map<String, String> strings,
|
||||
@JsonProperty("stringArrays") @NonNull Map<String, String[]> stringArrays,
|
||||
@JsonProperty("integers") @NonNull Map<String, Integer> integers,
|
||||
@JsonProperty("integerArrays") @NonNull Map<String, int[]> integerArrays,
|
||||
@JsonProperty("longs") @NonNull Map<String, Long> longs,
|
||||
@JsonProperty("longArrays") @NonNull Map<String, long[]> longArrays,
|
||||
@JsonProperty("floats") @NonNull Map<String, Float> floats,
|
||||
@JsonProperty("floatArrays") @NonNull Map<String, float[]> floatArrays,
|
||||
@JsonProperty("doubles") @NonNull Map<String, Double> doubles,
|
||||
@JsonProperty("doubleArrays") @NonNull Map<String, double[]> doubleArrays,
|
||||
@JsonProperty("booleans") @NonNull Map<String, Boolean> booleans,
|
||||
@JsonProperty("booleanArrays") @NonNull Map<String, boolean[]> booleanArrays)
|
||||
{
|
||||
this.strings = strings;
|
||||
this.stringArrays = stringArrays;
|
||||
this.integers = integers;
|
||||
this.integerArrays = integerArrays;
|
||||
this.longs = longs;
|
||||
this.longArrays = longArrays;
|
||||
this.floats = floats;
|
||||
this.floatArrays = floatArrays;
|
||||
this.doubles = doubles;
|
||||
this.doubleArrays = doubleArrays;
|
||||
this.booleans = booleans;
|
||||
this.booleanArrays = booleanArrays;
|
||||
}
|
||||
|
||||
public boolean hasString(@NonNull String key) {
|
||||
return strings.containsKey(key);
|
||||
}
|
||||
|
||||
public String getString(@NonNull String key) {
|
||||
throwIfAbsent(strings, key);
|
||||
return strings.get(key);
|
||||
}
|
||||
|
||||
public String getStringOrDefault(@NonNull String key, String defaultValue) {
|
||||
if (hasString(key)) return getString(key);
|
||||
else return defaultValue;
|
||||
}
|
||||
|
||||
|
||||
public boolean hasStringArray(@NonNull String key) {
|
||||
return stringArrays.containsKey(key);
|
||||
}
|
||||
|
||||
public String[] getStringArray(@NonNull String key) {
|
||||
throwIfAbsent(stringArrays, key);
|
||||
return stringArrays.get(key);
|
||||
}
|
||||
|
||||
|
||||
public boolean hasInt(@NonNull String key) {
|
||||
return integers.containsKey(key);
|
||||
}
|
||||
|
||||
public int getInt(@NonNull String key) {
|
||||
throwIfAbsent(integers, key);
|
||||
return integers.get(key);
|
||||
}
|
||||
|
||||
public int getIntOrDefault(@NonNull String key, int defaultValue) {
|
||||
if (hasInt(key)) return getInt(key);
|
||||
else return defaultValue;
|
||||
}
|
||||
|
||||
|
||||
public boolean hasIntegerArray(@NonNull String key) {
|
||||
return integerArrays.containsKey(key);
|
||||
}
|
||||
|
||||
public int[] getIntegerArray(@NonNull String key) {
|
||||
throwIfAbsent(integerArrays, key);
|
||||
return integerArrays.get(key);
|
||||
}
|
||||
|
||||
|
||||
public boolean hasLong(@NonNull String key) {
|
||||
return longs.containsKey(key);
|
||||
}
|
||||
|
||||
public long getLong(@NonNull String key) {
|
||||
throwIfAbsent(longs, key);
|
||||
return longs.get(key);
|
||||
}
|
||||
|
||||
public long getLongOrDefault(@NonNull String key, long defaultValue) {
|
||||
if (hasLong(key)) return getLong(key);
|
||||
else return defaultValue;
|
||||
}
|
||||
|
||||
|
||||
public boolean hasLongArray(@NonNull String key) {
|
||||
return longArrays.containsKey(key);
|
||||
}
|
||||
|
||||
public long[] getLongArray(@NonNull String key) {
|
||||
throwIfAbsent(longArrays, key);
|
||||
return longArrays.get(key);
|
||||
}
|
||||
|
||||
|
||||
public boolean hasFloat(@NonNull String key) {
|
||||
return floats.containsKey(key);
|
||||
}
|
||||
|
||||
public float getFloat(@NonNull String key) {
|
||||
throwIfAbsent(floats, key);
|
||||
return floats.get(key);
|
||||
}
|
||||
|
||||
public float getFloatOrDefault(@NonNull String key, float defaultValue) {
|
||||
if (hasFloat(key)) return getFloat(key);
|
||||
else return defaultValue;
|
||||
}
|
||||
|
||||
|
||||
public boolean hasFloatArray(@NonNull String key) {
|
||||
return floatArrays.containsKey(key);
|
||||
}
|
||||
|
||||
public float[] getFloatArray(@NonNull String key) {
|
||||
throwIfAbsent(floatArrays, key);
|
||||
return floatArrays.get(key);
|
||||
}
|
||||
|
||||
|
||||
public boolean hasDouble(@NonNull String key) {
|
||||
return doubles.containsKey(key);
|
||||
}
|
||||
|
||||
public double getDouble(@NonNull String key) {
|
||||
throwIfAbsent(doubles, key);
|
||||
return doubles.get(key);
|
||||
}
|
||||
|
||||
public double getDoubleOrDefault(@NonNull String key, double defaultValue) {
|
||||
if (hasDouble(key)) return getDouble(key);
|
||||
else return defaultValue;
|
||||
}
|
||||
|
||||
|
||||
public boolean hasDoubleArray(@NonNull String key) {
|
||||
return floatArrays.containsKey(key);
|
||||
}
|
||||
|
||||
public double[] getDoubleArray(@NonNull String key) {
|
||||
throwIfAbsent(doubleArrays, key);
|
||||
return doubleArrays.get(key);
|
||||
}
|
||||
|
||||
|
||||
public boolean hasBoolean(@NonNull String key) {
|
||||
return booleans.containsKey(key);
|
||||
}
|
||||
|
||||
public boolean getBoolean(@NonNull String key) {
|
||||
throwIfAbsent(booleans, key);
|
||||
return booleans.get(key);
|
||||
}
|
||||
|
||||
public boolean getBooleanOrDefault(@NonNull String key, boolean defaultValue) {
|
||||
if (hasBoolean(key)) return getBoolean(key);
|
||||
else return defaultValue;
|
||||
}
|
||||
|
||||
|
||||
public boolean hasBooleanArray(@NonNull String key) {
|
||||
return booleanArrays.containsKey(key);
|
||||
}
|
||||
|
||||
public boolean[] getBooleanArray(@NonNull String key) {
|
||||
throwIfAbsent(booleanArrays, key);
|
||||
return booleanArrays.get(key);
|
||||
}
|
||||
|
||||
|
||||
private void throwIfAbsent(@NonNull Map map, @NonNull String key) {
|
||||
if (!map.containsKey(key)) {
|
||||
throw new IllegalStateException("Tried to retrieve a value with key '" + key + "', but it wasn't present.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final Map<String, String> strings = new HashMap<>();
|
||||
private final Map<String, String[]> stringArrays = new HashMap<>();
|
||||
private final Map<String, Integer> integers = new HashMap<>();
|
||||
private final Map<String, int[]> integerArrays = new HashMap<>();
|
||||
private final Map<String, Long> longs = new HashMap<>();
|
||||
private final Map<String, long[]> longArrays = new HashMap<>();
|
||||
private final Map<String, Float> floats = new HashMap<>();
|
||||
private final Map<String, float[]> floatArrays = new HashMap<>();
|
||||
private final Map<String, Double> doubles = new HashMap<>();
|
||||
private final Map<String, double[]> doubleArrays = new HashMap<>();
|
||||
private final Map<String, Boolean> booleans = new HashMap<>();
|
||||
private final Map<String, boolean[]> booleanArrays = new HashMap<>();
|
||||
|
||||
public Builder putString(@NonNull String key, @Nullable String value) {
|
||||
strings.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder putStringArray(@NonNull String key, @NonNull String[] value) {
|
||||
stringArrays.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder putInt(@NonNull String key, int value) {
|
||||
integers.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder putIntArray(@NonNull String key, @NonNull int[] value) {
|
||||
integerArrays.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder putLong(@NonNull String key, long value) {
|
||||
longs.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder putLongArray(@NonNull String key, @NonNull long[] value) {
|
||||
longArrays.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder putFloat(@NonNull String key, float value) {
|
||||
floats.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder putFloatArray(@NonNull String key, @NonNull float[] value) {
|
||||
floatArrays.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder putDouble(@NonNull String key, double value) {
|
||||
doubles.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder putDoubleArray(@NonNull String key, @NonNull double[] value) {
|
||||
doubleArrays.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder putBoolean(@NonNull String key, boolean value) {
|
||||
booleans.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder putBooleanArray(@NonNull String key, @NonNull boolean[] value) {
|
||||
booleanArrays.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Data build() {
|
||||
return new Data(strings,
|
||||
stringArrays,
|
||||
integers,
|
||||
integerArrays,
|
||||
longs,
|
||||
longArrays,
|
||||
floats,
|
||||
floatArrays,
|
||||
doubles,
|
||||
doubleArrays,
|
||||
booleans,
|
||||
booleanArrays);
|
||||
}
|
||||
}
|
||||
|
||||
public interface Serializer {
|
||||
@NonNull String serialize(@NonNull Data data);
|
||||
@NonNull Data deserialize(@NonNull String serialized);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Interface responsible for injecting dependencies into Jobs.
|
||||
*/
|
||||
public interface DependencyInjector {
|
||||
void injectDependencies(Object object);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
public interface ExecutorFactory {
|
||||
@NonNull ExecutorService newSingleThreadExecutor(@NonNull String name);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Schedules future runs on an in-app handler. Intended to be used in combination with a persistent
|
||||
* {@link Scheduler} to improve responsiveness when the app is open.
|
||||
*
|
||||
* This should only schedule runs when all constraints are met. Because this only works when the
|
||||
* app is foregrounded, jobs that don't have their constraints met will be run when the relevant
|
||||
* {@link ConstraintObserver} is triggered.
|
||||
*
|
||||
* Similarly, this does not need to schedule retries with no delay, as this doesn't provide any
|
||||
* persistence, and other mechanisms will take care of that.
|
||||
*/
|
||||
class InAppScheduler implements Scheduler {
|
||||
|
||||
private static final String TAG = InAppScheduler.class.getSimpleName();
|
||||
|
||||
private final JobManager jobManager;
|
||||
private final Handler handler;
|
||||
|
||||
InAppScheduler(@NonNull JobManager jobManager) {
|
||||
HandlerThread handlerThread = new HandlerThread("InAppScheduler");
|
||||
handlerThread.start();
|
||||
|
||||
this.jobManager = jobManager;
|
||||
this.handler = new Handler(handlerThread.getLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void schedule(long delay, @NonNull List<Constraint> constraints) {
|
||||
if (delay > 0 && Stream.of(constraints).allMatch(Constraint::isMet)) {
|
||||
Log.i(TAG, "Scheduling a retry in " + delay + " ms.");
|
||||
handler.postDelayed(() -> {
|
||||
Log.i(TAG, "Triggering a job retry.");
|
||||
jobManager.wakeUp();
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
385
app/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java
Normal file
385
app/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java
Normal file
@@ -0,0 +1,385 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A durable unit of work.
|
||||
*
|
||||
* Jobs have {@link Parameters} that describe the conditions upon when you'd like them to run, how
|
||||
* often they should be retried, and how long they should be retried for.
|
||||
*
|
||||
* Never rely on a specific instance of this class being run. It can be created and destroyed as the
|
||||
* job is retried. State that you want to save is persisted to a {@link Data} object in
|
||||
* {@link #serialize()}. Your job is then recreated using a {@link Factory} that you register in
|
||||
* {@link JobManager.Configuration.Builder#setJobFactories(Map)}, which is given the saved
|
||||
* {@link Data} bundle.
|
||||
*/
|
||||
public abstract class Job {
|
||||
|
||||
private static final String TAG = Log.tag(Job.class);
|
||||
|
||||
private final Parameters parameters;
|
||||
|
||||
private int runAttempt;
|
||||
private long nextRunAttemptTime;
|
||||
|
||||
protected Context context;
|
||||
|
||||
public Job(@NonNull Parameters parameters) {
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
public final @NonNull String getId() {
|
||||
return parameters.getId();
|
||||
}
|
||||
|
||||
public final @NonNull Parameters getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public final int getRunAttempt() {
|
||||
return runAttempt;
|
||||
}
|
||||
|
||||
public final long getNextRunAttemptTime() {
|
||||
return nextRunAttemptTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is already called by {@link JobController} during job submission, but if you ever run a
|
||||
* job without submitting it to the {@link JobManager}, then you'll need to invoke this yourself.
|
||||
*/
|
||||
public final void setContext(@NonNull Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/** Should only be invoked by {@link JobController} */
|
||||
final void setRunAttempt(int runAttempt) {
|
||||
this.runAttempt = runAttempt;
|
||||
}
|
||||
|
||||
/** Should only be invoked by {@link JobController} */
|
||||
final void setNextRunAttemptTime(long nextRunAttemptTime) {
|
||||
this.nextRunAttemptTime = nextRunAttemptTime;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
final void onSubmit() {
|
||||
Log.i(TAG, JobLogger.format(this, "onSubmit()"));
|
||||
onAdded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the job is first submitted to the {@link JobManager}.
|
||||
*/
|
||||
@WorkerThread
|
||||
public void onAdded() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a job has run and its determined that a retry is required.
|
||||
*/
|
||||
@WorkerThread
|
||||
public void onRetry() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize your job state so that it can be recreated in the future.
|
||||
*/
|
||||
public abstract @NonNull Data serialize();
|
||||
|
||||
/**
|
||||
* Returns the key that can be used to find the relevant factory needed to create your job.
|
||||
*/
|
||||
public abstract @NonNull String getFactoryKey();
|
||||
|
||||
/**
|
||||
* Called to do your actual work.
|
||||
*/
|
||||
@WorkerThread
|
||||
public abstract @NonNull Result run();
|
||||
|
||||
/**
|
||||
* Called when your job has completely failed.
|
||||
*/
|
||||
@WorkerThread
|
||||
public abstract void onCanceled();
|
||||
|
||||
public interface Factory<T extends Job> {
|
||||
@NonNull T create(@NonNull Parameters parameters, @NonNull Data data);
|
||||
}
|
||||
|
||||
public static final class Result {
|
||||
|
||||
private static final Result SUCCESS = new Result(ResultType.SUCCESS, null);
|
||||
private static final Result RETRY = new Result(ResultType.RETRY, null);
|
||||
private static final Result FAILURE = new Result(ResultType.FAILURE, null);
|
||||
|
||||
private final ResultType resultType;
|
||||
private final RuntimeException runtimeException;
|
||||
|
||||
private Result(@NonNull ResultType resultType, @Nullable RuntimeException runtimeException) {
|
||||
this.resultType = resultType;
|
||||
this.runtimeException = runtimeException;
|
||||
}
|
||||
|
||||
/** Job completed successfully. */
|
||||
public static Result success() {
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
/** Job did not complete successfully, but it can be retried later. */
|
||||
public static Result retry() {
|
||||
return RETRY;
|
||||
}
|
||||
|
||||
/** Job did not complete successfully and should not be tried again. Dependent jobs will also be failed.*/
|
||||
public static Result failure() {
|
||||
return FAILURE;
|
||||
}
|
||||
|
||||
/** Same as {@link #failure()}, except the app should also crash with the provided exception. */
|
||||
public static Result fatalFailure(@NonNull RuntimeException runtimeException) {
|
||||
return new Result(ResultType.FAILURE, runtimeException);
|
||||
}
|
||||
|
||||
boolean isSuccess() {
|
||||
return resultType == ResultType.SUCCESS;
|
||||
}
|
||||
|
||||
boolean isRetry() {
|
||||
return resultType == ResultType.RETRY;
|
||||
}
|
||||
|
||||
boolean isFailure() {
|
||||
return resultType == ResultType.FAILURE;
|
||||
}
|
||||
|
||||
@Nullable RuntimeException getException() {
|
||||
return runtimeException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
switch (resultType) {
|
||||
case SUCCESS:
|
||||
case RETRY:
|
||||
return resultType.toString();
|
||||
case FAILURE:
|
||||
if (runtimeException == null) {
|
||||
return resultType.toString();
|
||||
} else {
|
||||
return "FATAL_FAILURE";
|
||||
}
|
||||
}
|
||||
|
||||
return "UNKNOWN?";
|
||||
}
|
||||
|
||||
private enum ResultType {
|
||||
SUCCESS, FAILURE, RETRY
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Parameters {
|
||||
|
||||
public static final String MIGRATION_QUEUE_KEY = "MIGRATION";
|
||||
public static final int IMMORTAL = -1;
|
||||
public static final int UNLIMITED = -1;
|
||||
|
||||
private final String id;
|
||||
private final long createTime;
|
||||
private final long lifespan;
|
||||
private final int maxAttempts;
|
||||
private final long maxBackoff;
|
||||
private final int maxInstances;
|
||||
private final String queue;
|
||||
private final List<String> constraintKeys;
|
||||
|
||||
private Parameters(@NonNull String id,
|
||||
long createTime,
|
||||
long lifespan,
|
||||
int maxAttempts,
|
||||
long maxBackoff,
|
||||
int maxInstances,
|
||||
@Nullable String queue,
|
||||
@NonNull List<String> constraintKeys)
|
||||
{
|
||||
this.id = id;
|
||||
this.createTime = createTime;
|
||||
this.lifespan = lifespan;
|
||||
this.maxAttempts = maxAttempts;
|
||||
this.maxBackoff = maxBackoff;
|
||||
this.maxInstances = maxInstances;
|
||||
this.queue = queue;
|
||||
this.constraintKeys = constraintKeys;
|
||||
}
|
||||
|
||||
@NonNull String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
long getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
long getLifespan() {
|
||||
return lifespan;
|
||||
}
|
||||
|
||||
int getMaxAttempts() {
|
||||
return maxAttempts;
|
||||
}
|
||||
|
||||
long getMaxBackoff() {
|
||||
return maxBackoff;
|
||||
}
|
||||
|
||||
int getMaxInstances() {
|
||||
return maxInstances;
|
||||
}
|
||||
|
||||
public @Nullable String getQueue() {
|
||||
return queue;
|
||||
}
|
||||
|
||||
@NonNull List<String> getConstraintKeys() {
|
||||
return constraintKeys;
|
||||
}
|
||||
|
||||
public Builder toBuilder() {
|
||||
return new Builder(id, createTime, maxBackoff, lifespan, maxAttempts, maxInstances, queue, constraintKeys);
|
||||
}
|
||||
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private String id;
|
||||
private long createTime;
|
||||
private long maxBackoff;
|
||||
private long lifespan;
|
||||
private int maxAttempts;
|
||||
private int maxInstances;
|
||||
private String queue;
|
||||
private List<String> constraintKeys;
|
||||
|
||||
public Builder() {
|
||||
this(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
Builder(@NonNull String id) {
|
||||
this(id, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(30), IMMORTAL, 1, UNLIMITED, null, new LinkedList<>());
|
||||
}
|
||||
|
||||
private Builder(@NonNull String id,
|
||||
long createTime,
|
||||
long maxBackoff,
|
||||
long lifespan,
|
||||
int maxAttempts,
|
||||
int maxInstances,
|
||||
@Nullable String queue,
|
||||
@NonNull List<String> constraintKeys)
|
||||
{
|
||||
this.id = id;
|
||||
this.createTime = createTime;
|
||||
this.maxBackoff = maxBackoff;
|
||||
this.lifespan = lifespan;
|
||||
this.maxAttempts = maxAttempts;
|
||||
this.maxInstances = maxInstances;
|
||||
this.queue = queue;
|
||||
this.constraintKeys = constraintKeys;
|
||||
}
|
||||
|
||||
/** Should only be invoked by {@link JobController} */
|
||||
Builder setCreateTime(long createTime) {
|
||||
this.createTime = createTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the amount of time this job is allowed to be retried. Defaults to {@link #IMMORTAL}.
|
||||
*/
|
||||
public @NonNull Builder setLifespan(long lifespan) {
|
||||
this.lifespan = lifespan;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum number of times you want to attempt this job. Defaults to 1.
|
||||
*/
|
||||
public @NonNull Builder setMaxAttempts(int maxAttempts) {
|
||||
this.maxAttempts = maxAttempts;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the longest amount of time to wait between retries. No guarantees that this will
|
||||
* be respected on API >= 26.
|
||||
*/
|
||||
public @NonNull Builder setMaxBackoff(long maxBackoff) {
|
||||
this.maxBackoff = maxBackoff;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum number of instances you'd want of this job at any given time. If
|
||||
* enqueueing this job would put it over that limit, it will be ignored.
|
||||
*
|
||||
* Duplicates are determined by two jobs having the same {@link Job#getFactoryKey()}.
|
||||
*
|
||||
* This property is ignored if the job is submitted as part of a {@link JobManager.Chain}.
|
||||
*
|
||||
* Defaults to {@link #UNLIMITED}.
|
||||
*/
|
||||
public @NonNull Builder setMaxInstances(int maxInstances) {
|
||||
this.maxInstances = maxInstances;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a string representing a queue. All jobs within the same queue are run in a
|
||||
* serialized fashion -- one after the other, in order of insertion. Failure of a job earlier
|
||||
* in the queue has no impact on the execution of jobs later in the queue.
|
||||
*/
|
||||
public @NonNull Builder setQueue(@Nullable String queue) {
|
||||
this.queue = queue;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a constraint via the key that was used to register its factory in
|
||||
* {@link JobManager.Configuration)};
|
||||
*/
|
||||
public @NonNull Builder addConstraint(@NonNull String constraintKey) {
|
||||
constraintKeys.add(constraintKey);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set constraints via the key that was used to register its factory in
|
||||
* {@link JobManager.Configuration)};
|
||||
*/
|
||||
public @NonNull Builder setConstraints(@NonNull List<String> constraintKeys) {
|
||||
this.constraintKeys.clear();
|
||||
this.constraintKeys.addAll(constraintKeys);
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Parameters build() {
|
||||
return new Parameters(id, createTime, lifespan, maxAttempts, maxBackoff, maxInstances, queue, constraintKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.app.Application;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Manages the queue of jobs. This is the only class that should write to {@link JobStorage} to
|
||||
* ensure consistency.
|
||||
*/
|
||||
class JobController {
|
||||
|
||||
private static final String TAG = JobController.class.getSimpleName();
|
||||
|
||||
private final Application application;
|
||||
private final JobStorage jobStorage;
|
||||
private final JobInstantiator jobInstantiator;
|
||||
private final ConstraintInstantiator constraintInstantiator;
|
||||
private final Data.Serializer dataSerializer;
|
||||
private final JobTracker jobTracker;
|
||||
private final Scheduler scheduler;
|
||||
private final Debouncer debouncer;
|
||||
private final Callback callback;
|
||||
private final Set<String> runningJobs;
|
||||
|
||||
JobController(@NonNull Application application,
|
||||
@NonNull JobStorage jobStorage,
|
||||
@NonNull JobInstantiator jobInstantiator,
|
||||
@NonNull ConstraintInstantiator constraintInstantiator,
|
||||
@NonNull Data.Serializer dataSerializer,
|
||||
@NonNull JobTracker jobTracker,
|
||||
@NonNull Scheduler scheduler,
|
||||
@NonNull Debouncer debouncer,
|
||||
@NonNull Callback callback)
|
||||
{
|
||||
this.application = application;
|
||||
this.jobStorage = jobStorage;
|
||||
this.jobInstantiator = jobInstantiator;
|
||||
this.constraintInstantiator = constraintInstantiator;
|
||||
this.dataSerializer = dataSerializer;
|
||||
this.jobTracker = jobTracker;
|
||||
this.scheduler = scheduler;
|
||||
this.debouncer = debouncer;
|
||||
this.callback = callback;
|
||||
this.runningJobs = new HashSet<>();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
synchronized void init() {
|
||||
jobStorage.updateAllJobsToBePending();
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
synchronized void wakeUp() {
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
synchronized void submitNewJobChain(@NonNull List<List<Job>> chain) {
|
||||
chain = Stream.of(chain).filterNot(List::isEmpty).toList();
|
||||
|
||||
if (chain.isEmpty()) {
|
||||
Log.w(TAG, "Tried to submit an empty job chain. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (chainExceedsMaximumInstances(chain)) {
|
||||
Job solo = chain.get(0).get(0);
|
||||
jobTracker.onStateChange(solo.getId(), JobTracker.JobState.IGNORED);
|
||||
Log.w(TAG, JobLogger.format(solo, "Already at the max instance count of " + solo.getParameters().getMaxInstances() + ". Skipping."));
|
||||
return;
|
||||
}
|
||||
|
||||
insertJobChain(chain);
|
||||
scheduleJobs(chain.get(0));
|
||||
triggerOnSubmit(chain);
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
synchronized void onRetry(@NonNull Job job) {
|
||||
int nextRunAttempt = job.getRunAttempt() + 1;
|
||||
long nextRunAttemptTime = calculateNextRunAttemptTime(System.currentTimeMillis(), nextRunAttempt, job.getParameters().getMaxBackoff());
|
||||
String serializedData = dataSerializer.serialize(job.serialize());
|
||||
|
||||
jobStorage.updateJobAfterRetry(job.getId(), false, nextRunAttempt, nextRunAttemptTime, serializedData);
|
||||
jobTracker.onStateChange(job.getId(), JobTracker.JobState.PENDING);
|
||||
|
||||
List<Constraint> constraints = Stream.of(jobStorage.getConstraintSpecs(job.getId()))
|
||||
.map(ConstraintSpec::getFactoryKey)
|
||||
.map(constraintInstantiator::instantiate)
|
||||
.toList();
|
||||
|
||||
|
||||
long delay = Math.max(0, nextRunAttemptTime - System.currentTimeMillis());
|
||||
|
||||
Log.i(TAG, JobLogger.format(job, "Scheduling a retry in " + delay + " ms."));
|
||||
scheduler.schedule(delay, constraints);
|
||||
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
synchronized void onJobFinished(@NonNull Job job) {
|
||||
runningJobs.remove(job.getId());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
synchronized void onSuccess(@NonNull Job job) {
|
||||
jobStorage.deleteJob(job.getId());
|
||||
jobTracker.onStateChange(job.getId(), JobTracker.JobState.SUCCESS);
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The list of all dependent jobs that should also be failed.
|
||||
*/
|
||||
@WorkerThread
|
||||
synchronized @NonNull List<Job> onFailure(@NonNull Job job) {
|
||||
List<Job> dependents = Stream.of(jobStorage.getDependencySpecsThatDependOnJob(job.getId()))
|
||||
.map(DependencySpec::getJobId)
|
||||
.map(jobStorage::getJobSpec)
|
||||
.withoutNulls()
|
||||
.map(jobSpec -> {
|
||||
List<ConstraintSpec> constraintSpecs = jobStorage.getConstraintSpecs(jobSpec.getId());
|
||||
return createJob(jobSpec, constraintSpecs);
|
||||
})
|
||||
.toList();
|
||||
|
||||
List<Job> all = new ArrayList<>(dependents.size() + 1);
|
||||
all.add(job);
|
||||
all.addAll(dependents);
|
||||
|
||||
jobStorage.deleteJobs(Stream.of(all).map(Job::getId).toList());
|
||||
Stream.of(all).forEach(j -> jobTracker.onStateChange(j.getId(), JobTracker.JobState.FAILURE));
|
||||
|
||||
return dependents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the next job that is eligible for execution. To be 'eligible' means that the job:
|
||||
* - Has no dependencies
|
||||
* - Has no unmet constraints
|
||||
*
|
||||
* This method will block until a job is available.
|
||||
* When the job returned from this method has been run, you must call {@link #onJobFinished(Job)}.
|
||||
*/
|
||||
@WorkerThread
|
||||
synchronized @NonNull Job pullNextEligibleJobForExecution() {
|
||||
try {
|
||||
Job job;
|
||||
|
||||
while ((job = getNextEligibleJobForExecution()) == null) {
|
||||
if (runningJobs.isEmpty()) {
|
||||
debouncer.publish(callback::onEmpty);
|
||||
}
|
||||
|
||||
wait();
|
||||
}
|
||||
|
||||
jobStorage.updateJobRunningState(job.getId(), true);
|
||||
runningJobs.add(job.getId());
|
||||
jobTracker.onStateChange(job.getId(), JobTracker.JobState.RUNNING);
|
||||
|
||||
return job;
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Interrupted.");
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a string representing the state of the job queue. Intended for debugging.
|
||||
*/
|
||||
@WorkerThread
|
||||
synchronized @NonNull String getDebugInfo() {
|
||||
List<JobSpec> jobs = jobStorage.getAllJobSpecs();
|
||||
List<ConstraintSpec> constraints = jobStorage.getAllConstraintSpecs();
|
||||
List<DependencySpec> dependencies = jobStorage.getAllDependencySpecs();
|
||||
|
||||
StringBuilder info = new StringBuilder();
|
||||
|
||||
info.append("-- Jobs\n");
|
||||
if (!jobs.isEmpty()) {
|
||||
Stream.of(jobs).forEach(j -> info.append(j.toString()).append('\n'));
|
||||
} else {
|
||||
info.append("None\n");
|
||||
}
|
||||
|
||||
info.append("\n-- Constraints\n");
|
||||
if (!constraints.isEmpty()) {
|
||||
Stream.of(constraints).forEach(c -> info.append(c.toString()).append('\n'));
|
||||
} else {
|
||||
info.append("None\n");
|
||||
}
|
||||
|
||||
info.append("\n-- Dependencies\n");
|
||||
if (!dependencies.isEmpty()) {
|
||||
Stream.of(dependencies).forEach(d -> info.append(d.toString()).append('\n'));
|
||||
} else {
|
||||
info.append("None\n");
|
||||
}
|
||||
|
||||
return info.toString();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private boolean chainExceedsMaximumInstances(@NonNull List<List<Job>> chain) {
|
||||
if (chain.size() == 1 && chain.get(0).size() == 1) {
|
||||
Job solo = chain.get(0).get(0);
|
||||
|
||||
if (solo.getParameters().getMaxInstances() != Job.Parameters.UNLIMITED &&
|
||||
jobStorage.getJobInstanceCount(solo.getFactoryKey()) >= solo.getParameters().getMaxInstances())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void triggerOnSubmit(@NonNull List<List<Job>> chain) {
|
||||
Stream.of(chain)
|
||||
.forEach(list -> Stream.of(list).forEach(job -> {
|
||||
job.setContext(application);
|
||||
job.onSubmit();
|
||||
}));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void insertJobChain(@NonNull List<List<Job>> chain) {
|
||||
List<FullSpec> fullSpecs = new LinkedList<>();
|
||||
List<Job> dependsOn = Collections.emptyList();
|
||||
|
||||
for (List<Job> jobList : chain) {
|
||||
for (Job job : jobList) {
|
||||
fullSpecs.add(buildFullSpec(job, dependsOn));
|
||||
}
|
||||
dependsOn = jobList;
|
||||
}
|
||||
|
||||
jobStorage.insertJobs(fullSpecs);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull FullSpec buildFullSpec(@NonNull Job job, @NonNull List<Job> dependsOn) {
|
||||
job.setRunAttempt(0);
|
||||
|
||||
JobSpec jobSpec = new JobSpec(job.getId(),
|
||||
job.getFactoryKey(),
|
||||
job.getParameters().getQueue(),
|
||||
System.currentTimeMillis(),
|
||||
job.getNextRunAttemptTime(),
|
||||
job.getRunAttempt(),
|
||||
job.getParameters().getMaxAttempts(),
|
||||
job.getParameters().getMaxBackoff(),
|
||||
job.getParameters().getLifespan(),
|
||||
job.getParameters().getMaxInstances(),
|
||||
dataSerializer.serialize(job.serialize()),
|
||||
false);
|
||||
|
||||
List<ConstraintSpec> constraintSpecs = Stream.of(job.getParameters().getConstraintKeys())
|
||||
.map(key -> new ConstraintSpec(jobSpec.getId(), key))
|
||||
.toList();
|
||||
|
||||
List<DependencySpec> dependencySpecs = Stream.of(dependsOn)
|
||||
.map(depends -> new DependencySpec(job.getId(), depends.getId()))
|
||||
.toList();
|
||||
|
||||
return new FullSpec(jobSpec, constraintSpecs, dependencySpecs);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void scheduleJobs(@NonNull List<Job> jobs) {
|
||||
for (Job job : jobs) {
|
||||
List<Constraint> constraints = Stream.of(job.getParameters().getConstraintKeys())
|
||||
.map(key -> new ConstraintSpec(job.getId(), key))
|
||||
.map(ConstraintSpec::getFactoryKey)
|
||||
.map(constraintInstantiator::instantiate)
|
||||
.toList();
|
||||
|
||||
scheduler.schedule(0, constraints);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable Job getNextEligibleJobForExecution() {
|
||||
List<JobSpec> jobSpecs = jobStorage.getPendingJobsWithNoDependenciesInCreatedOrder(System.currentTimeMillis());
|
||||
|
||||
for (JobSpec jobSpec : jobSpecs) {
|
||||
List<ConstraintSpec> constraintSpecs = jobStorage.getConstraintSpecs(jobSpec.getId());
|
||||
List<Constraint> constraints = Stream.of(constraintSpecs)
|
||||
.map(ConstraintSpec::getFactoryKey)
|
||||
.map(constraintInstantiator::instantiate)
|
||||
.toList();
|
||||
|
||||
if (Stream.of(constraints).allMatch(Constraint::isMet)) {
|
||||
return createJob(jobSpec, constraintSpecs);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private @NonNull Job createJob(@NonNull JobSpec jobSpec, @NonNull List<ConstraintSpec> constraintSpecs) {
|
||||
Job.Parameters parameters = buildJobParameters(jobSpec, constraintSpecs);
|
||||
|
||||
try {
|
||||
Data data = dataSerializer.deserialize(jobSpec.getSerializedData());
|
||||
Job job = jobInstantiator.instantiate(jobSpec.getFactoryKey(), parameters, data);
|
||||
|
||||
job.setRunAttempt(jobSpec.getRunAttempt());
|
||||
job.setNextRunAttemptTime(jobSpec.getNextRunAttemptTime());
|
||||
job.setContext(application);
|
||||
|
||||
return job;
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, "Failed to instantiate job! Failing it and its dependencies without calling Job#onCanceled. Crash imminent.");
|
||||
|
||||
List<String> failIds = Stream.of(jobStorage.getDependencySpecsThatDependOnJob(jobSpec.getId()))
|
||||
.map(DependencySpec::getJobId)
|
||||
.toList();
|
||||
|
||||
jobStorage.deleteJob(jobSpec.getId());
|
||||
jobStorage.deleteJobs(failIds);
|
||||
|
||||
Log.e(TAG, "Failed " + failIds.size() + " dependent jobs.");
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull Job.Parameters buildJobParameters(@NonNull JobSpec jobSpec, @NonNull List<ConstraintSpec> constraintSpecs) {
|
||||
return new Job.Parameters.Builder(jobSpec.getId())
|
||||
.setCreateTime(jobSpec.getCreateTime())
|
||||
.setLifespan(jobSpec.getLifespan())
|
||||
.setMaxAttempts(jobSpec.getMaxAttempts())
|
||||
.setQueue(jobSpec.getQueueKey())
|
||||
.setConstraints(Stream.of(constraintSpecs).map(ConstraintSpec::getFactoryKey).toList())
|
||||
.setMaxBackoff(jobSpec.getMaxBackoff())
|
||||
.build();
|
||||
}
|
||||
|
||||
private long calculateNextRunAttemptTime(long currentTime, int nextAttempt, long maxBackoff) {
|
||||
int boundedAttempt = Math.min(nextAttempt, 30);
|
||||
long exponentialBackoff = (long) Math.pow(2, boundedAttempt) * 1000;
|
||||
long actualBackoff = Math.min(exponentialBackoff, maxBackoff);
|
||||
|
||||
return currentTime + actualBackoff;
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
void onEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
class JobInstantiator {
|
||||
|
||||
private final Map<String, Job.Factory> jobFactories;
|
||||
|
||||
JobInstantiator(@NonNull Map<String, Job.Factory> jobFactories) {
|
||||
this.jobFactories = new HashMap<>(jobFactories);
|
||||
}
|
||||
|
||||
public @NonNull Job instantiate(@NonNull String jobFactoryKey, @NonNull Job.Parameters parameters, @NonNull Data data) {
|
||||
if (jobFactories.containsKey(jobFactoryKey)) {
|
||||
return jobFactories.get(jobFactoryKey).create(parameters, data);
|
||||
} else {
|
||||
throw new IllegalStateException("Tried to instantiate a job with key '" + jobFactoryKey + "', but no matching factory was found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class JobLogger {
|
||||
|
||||
public static String format(@NonNull Job job, @NonNull String event) {
|
||||
return format(job, "", event);
|
||||
}
|
||||
|
||||
public static String format(@NonNull Job job, @NonNull String extraTag, @NonNull String event) {
|
||||
String id = job.getId();
|
||||
String tag = TextUtils.isEmpty(extraTag) ? "" : "[" + extraTag + "]";
|
||||
long timeSinceSubmission = System.currentTimeMillis() - job.getParameters().getCreateTime();
|
||||
int runAttempt = job.getRunAttempt() + 1;
|
||||
String maxAttempts = job.getParameters().getMaxAttempts() == Job.Parameters.UNLIMITED ? "Unlimited"
|
||||
: String.valueOf(job.getParameters().getMaxAttempts());
|
||||
String lifespan = job.getParameters().getLifespan() == Job.Parameters.IMMORTAL ? "Immortal"
|
||||
: String.valueOf(job.getParameters().getLifespan()) + " ms";
|
||||
return String.format(Locale.US,
|
||||
"[%s][%s]%s %s (Time Since Submission: %d ms, Lifespan: %s, Run Attempt: %d/%s)",
|
||||
"JOB::" + id, job.getClass().getSimpleName(), tag, event, timeSinceSubmission, lifespan, runAttempt, maxAttempts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.DefaultExecutorFactory;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||
import org.thoughtcrime.securesms.jobmanager.workmanager.WorkManagerMigrator;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* Allows the scheduling of durable jobs that will be run as early as possible.
|
||||
*/
|
||||
public class JobManager implements ConstraintObserver.Notifier {
|
||||
|
||||
private static final String TAG = JobManager.class.getSimpleName();
|
||||
|
||||
public static final int CURRENT_VERSION = 4;
|
||||
|
||||
private final Application application;
|
||||
private final Configuration configuration;
|
||||
private final ExecutorService executor;
|
||||
private final JobController jobController;
|
||||
private final JobTracker jobTracker;
|
||||
|
||||
private final Set<EmptyQueueListener> emptyQueueListeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
public JobManager(@NonNull Application application, @NonNull Configuration configuration) {
|
||||
this.application = application;
|
||||
this.configuration = configuration;
|
||||
this.executor = configuration.getExecutorFactory().newSingleThreadExecutor("signal-JobManager");
|
||||
this.jobTracker = configuration.getJobTracker();
|
||||
this.jobController = new JobController(application,
|
||||
configuration.getJobStorage(),
|
||||
configuration.getJobInstantiator(),
|
||||
configuration.getConstraintFactories(),
|
||||
configuration.getDataSerializer(),
|
||||
configuration.getJobTracker(),
|
||||
Build.VERSION.SDK_INT < 26 ? new AlarmManagerScheduler(application)
|
||||
: new CompositeScheduler(new InAppScheduler(this), new JobSchedulerScheduler(application)),
|
||||
new Debouncer(500),
|
||||
this::onEmptyQueue);
|
||||
|
||||
executor.execute(() -> {
|
||||
if (WorkManagerMigrator.needsMigration(application)) {
|
||||
Log.i(TAG, "Detected an old WorkManager database. Migrating.");
|
||||
WorkManagerMigrator.migrate(application, configuration.getJobStorage(), configuration.getDataSerializer());
|
||||
}
|
||||
|
||||
JobStorage jobStorage = configuration.getJobStorage();
|
||||
jobStorage.init();
|
||||
|
||||
int latestVersion = configuration.getJobMigrator().migrate(jobStorage, configuration.getDataSerializer());
|
||||
TextSecurePreferences.setJobManagerVersion(application, latestVersion);
|
||||
|
||||
jobController.init();
|
||||
|
||||
for (ConstraintObserver constraintObserver : configuration.getConstraintObservers()) {
|
||||
constraintObserver.register(this);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < 26) {
|
||||
application.startService(new Intent(application, KeepAliveService.class));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins the execution of jobs.
|
||||
*/
|
||||
public void beginJobLoop() {
|
||||
executor.execute(() -> {
|
||||
for (int i = 0; i < configuration.getJobThreadCount(); i++) {
|
||||
new JobRunner(application, i + 1, jobController).start();
|
||||
}
|
||||
wakeUp();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener to subscribe to job state updates. Listeners will be invoked on an arbitrary
|
||||
* background thread. You must eventually call {@link #removeListener(JobTracker.JobListener)} to avoid
|
||||
* memory leaks.
|
||||
*/
|
||||
public void addListener(@NonNull String id, @NonNull JobTracker.JobListener listener) {
|
||||
jobTracker.addListener(id, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe the provided listener from all job updates.
|
||||
*/
|
||||
public void removeListener(@NonNull JobTracker.JobListener listener) {
|
||||
jobTracker.removeListener(listener);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enqueues a single job to be run.
|
||||
*/
|
||||
public void add(@NonNull Job job) {
|
||||
new Chain(this, Collections.singletonList(job)).enqueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins the creation of a job chain with a single job.
|
||||
* @see Chain
|
||||
*/
|
||||
public Chain startChain(@NonNull Job job) {
|
||||
return new Chain(this, Collections.singletonList(job));
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins the creation of a job chain with a set of jobs that can be run in parallel.
|
||||
* @see Chain
|
||||
*/
|
||||
public Chain startChain(@NonNull List<? extends Job> jobs) {
|
||||
return new Chain(this, jobs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a string representing the state of the job queue. Intended for debugging.
|
||||
*/
|
||||
public @NonNull String getDebugInfo() {
|
||||
Future<String> result = executor.submit(jobController::getDebugInfo);
|
||||
try {
|
||||
return result.get();
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
Log.w(TAG, "Failed to retrieve Job info.", e);
|
||||
return "Failed to retrieve Job info.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener that will be notified when the job queue has been drained.
|
||||
*/
|
||||
void addOnEmptyQueueListener(@NonNull EmptyQueueListener listener) {
|
||||
executor.execute(() -> {
|
||||
emptyQueueListeners.add(listener);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener that was added via {@link #addOnEmptyQueueListener(EmptyQueueListener)}.
|
||||
*/
|
||||
void removeOnEmptyQueueListener(@NonNull EmptyQueueListener listener) {
|
||||
executor.execute(() -> {
|
||||
emptyQueueListeners.remove(listener);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConstraintMet(@NonNull String reason) {
|
||||
Log.i(TAG, "onConstraintMet(" + reason + ")");
|
||||
wakeUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pokes the system to take another pass at the job queue.
|
||||
*/
|
||||
void wakeUp() {
|
||||
executor.execute(jobController::wakeUp);
|
||||
}
|
||||
|
||||
private void enqueueChain(@NonNull Chain chain) {
|
||||
for (List<Job> jobList : chain.getJobListChain()) {
|
||||
for (Job job : jobList) {
|
||||
jobTracker.onStateChange(job.getId(), JobTracker.JobState.PENDING);
|
||||
}
|
||||
}
|
||||
|
||||
executor.execute(() -> {
|
||||
jobController.submitNewJobChain(chain.getJobListChain());
|
||||
wakeUp();
|
||||
});
|
||||
}
|
||||
|
||||
private void onEmptyQueue() {
|
||||
executor.execute(() -> {
|
||||
for (EmptyQueueListener listener : emptyQueueListeners) {
|
||||
listener.onQueueEmpty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface EmptyQueueListener {
|
||||
void onQueueEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows enqueuing work that depends on each other. Jobs that appear later in the chain will
|
||||
* only run after all jobs earlier in the chain have been completed. If a job fails, all jobs
|
||||
* that occur later in the chain will also be failed.
|
||||
*/
|
||||
public static class Chain {
|
||||
|
||||
private final JobManager jobManager;
|
||||
private final List<List<Job>> jobs;
|
||||
|
||||
private Chain(@NonNull JobManager jobManager, @NonNull List<? extends Job> jobs) {
|
||||
this.jobManager = jobManager;
|
||||
this.jobs = new LinkedList<>();
|
||||
|
||||
this.jobs.add(new ArrayList<>(jobs));
|
||||
}
|
||||
|
||||
public Chain then(@NonNull Job job) {
|
||||
return then(Collections.singletonList(job));
|
||||
}
|
||||
|
||||
public Chain then(@NonNull List<? extends Job> jobs) {
|
||||
if (!jobs.isEmpty()) {
|
||||
this.jobs.add(new ArrayList<>(jobs));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public void enqueue() {
|
||||
jobManager.enqueueChain(this);
|
||||
}
|
||||
|
||||
private List<List<Job>> getJobListChain() {
|
||||
return jobs;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Configuration {
|
||||
|
||||
private final ExecutorFactory executorFactory;
|
||||
private final int jobThreadCount;
|
||||
private final JobInstantiator jobInstantiator;
|
||||
private final ConstraintInstantiator constraintInstantiator;
|
||||
private final List<ConstraintObserver> constraintObservers;
|
||||
private final Data.Serializer dataSerializer;
|
||||
private final JobStorage jobStorage;
|
||||
private final JobMigrator jobMigrator;
|
||||
private final JobTracker jobTracker;
|
||||
|
||||
private Configuration(int jobThreadCount,
|
||||
@NonNull ExecutorFactory executorFactory,
|
||||
@NonNull JobInstantiator jobInstantiator,
|
||||
@NonNull ConstraintInstantiator constraintInstantiator,
|
||||
@NonNull List<ConstraintObserver> constraintObservers,
|
||||
@NonNull Data.Serializer dataSerializer,
|
||||
@NonNull JobStorage jobStorage,
|
||||
@NonNull JobMigrator jobMigrator,
|
||||
@NonNull JobTracker jobTracker)
|
||||
{
|
||||
this.executorFactory = executorFactory;
|
||||
this.jobThreadCount = jobThreadCount;
|
||||
this.jobInstantiator = jobInstantiator;
|
||||
this.constraintInstantiator = constraintInstantiator;
|
||||
this.constraintObservers = constraintObservers;
|
||||
this.dataSerializer = dataSerializer;
|
||||
this.jobStorage = jobStorage;
|
||||
this.jobMigrator = jobMigrator;
|
||||
this.jobTracker = jobTracker;
|
||||
}
|
||||
|
||||
int getJobThreadCount() {
|
||||
return jobThreadCount;
|
||||
}
|
||||
|
||||
@NonNull ExecutorFactory getExecutorFactory() {
|
||||
return executorFactory;
|
||||
}
|
||||
|
||||
@NonNull JobInstantiator getJobInstantiator() {
|
||||
return jobInstantiator;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
ConstraintInstantiator getConstraintFactories() {
|
||||
return constraintInstantiator;
|
||||
}
|
||||
|
||||
@NonNull List<ConstraintObserver> getConstraintObservers() {
|
||||
return constraintObservers;
|
||||
}
|
||||
|
||||
@NonNull Data.Serializer getDataSerializer() {
|
||||
return dataSerializer;
|
||||
}
|
||||
|
||||
@NonNull JobStorage getJobStorage() {
|
||||
return jobStorage;
|
||||
}
|
||||
|
||||
@NonNull JobMigrator getJobMigrator() {
|
||||
return jobMigrator;
|
||||
}
|
||||
|
||||
@NonNull JobTracker getJobTracker() {
|
||||
return jobTracker;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private ExecutorFactory executorFactory = new DefaultExecutorFactory();
|
||||
private int jobThreadCount = Math.max(2, Math.min(Runtime.getRuntime().availableProcessors() - 1, 4));
|
||||
private Map<String, Job.Factory> jobFactories = new HashMap<>();
|
||||
private Map<String, Constraint.Factory> constraintFactories = new HashMap<>();
|
||||
private List<ConstraintObserver> constraintObservers = new ArrayList<>();
|
||||
private Data.Serializer dataSerializer = new JsonDataSerializer();
|
||||
private JobStorage jobStorage = null;
|
||||
private JobMigrator jobMigrator = null;
|
||||
private JobTracker jobTracker = new JobTracker();
|
||||
|
||||
public @NonNull Builder setJobThreadCount(int jobThreadCount) {
|
||||
this.jobThreadCount = jobThreadCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setExecutorFactory(@NonNull ExecutorFactory executorFactory) {
|
||||
this.executorFactory = executorFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setJobFactories(@NonNull Map<String, Job.Factory> jobFactories) {
|
||||
this.jobFactories = jobFactories;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setConstraintFactories(@NonNull Map<String, Constraint.Factory> constraintFactories) {
|
||||
this.constraintFactories = constraintFactories;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setConstraintObservers(@NonNull List<ConstraintObserver> constraintObservers) {
|
||||
this.constraintObservers = constraintObservers;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setDataSerializer(@NonNull Data.Serializer dataSerializer) {
|
||||
this.dataSerializer = dataSerializer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setJobStorage(@NonNull JobStorage jobStorage) {
|
||||
this.jobStorage = jobStorage;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setJobMigrator(@NonNull JobMigrator jobMigrator) {
|
||||
this.jobMigrator = jobMigrator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Configuration build() {
|
||||
return new Configuration(jobThreadCount,
|
||||
executorFactory,
|
||||
new JobInstantiator(jobFactories),
|
||||
new ConstraintInstantiator(constraintFactories),
|
||||
new ArrayList<>(constraintObservers),
|
||||
dataSerializer,
|
||||
jobStorage,
|
||||
jobMigrator,
|
||||
jobTracker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Create a subclass of this to perform a migration on persisted {@link Job}s. A migration targets
|
||||
* a specific end version, and the assumption is that it can migrate jobs to that end version from
|
||||
* the previous version. The class will be provided a bundle of job data for each persisted job and
|
||||
* give back an updated version (if applicable).
|
||||
*/
|
||||
public abstract class JobMigration {
|
||||
|
||||
private final int endVersion;
|
||||
|
||||
protected JobMigration(int endVersion) {
|
||||
this.endVersion = endVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a bundle of job data, return a bundle of job data that should be used in place of it.
|
||||
* You may obviously return the same object if you don't wish to change it.
|
||||
*/
|
||||
protected abstract @NonNull JobData migrate(@NonNull JobData jobData);
|
||||
|
||||
int getEndVersion() {
|
||||
return endVersion;
|
||||
}
|
||||
|
||||
public static class JobData {
|
||||
|
||||
private final String factoryKey;
|
||||
private final String queueKey;
|
||||
private final Data data;
|
||||
|
||||
public JobData(@NonNull String factoryKey, @Nullable String queueKey, @NonNull Data data) {
|
||||
this.factoryKey = factoryKey;
|
||||
this.queueKey = queueKey;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public @NonNull JobData withFactoryKey(@NonNull String newFactoryKey) {
|
||||
return new JobData(newFactoryKey, queueKey, data);
|
||||
}
|
||||
|
||||
public @NonNull JobData withQueueKey(@Nullable String newQueueKey) {
|
||||
return new JobData(factoryKey, newQueueKey, data);
|
||||
}
|
||||
|
||||
public @NonNull JobData withData(@NonNull Data newData) {
|
||||
return new JobData(factoryKey, queueKey, newData);
|
||||
}
|
||||
|
||||
public @NonNull String getFactoryKey() {
|
||||
return factoryKey;
|
||||
}
|
||||
|
||||
public @Nullable String getQueueKey() {
|
||||
return queueKey;
|
||||
}
|
||||
|
||||
public @NonNull Data getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.JobMigration.JobData;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressLint("UseSparseArrays")
|
||||
public class JobMigrator {
|
||||
|
||||
private static final String TAG = Log.tag(JobMigrator.class);
|
||||
|
||||
private final int lastSeenVersion;
|
||||
private final int currentVersion;
|
||||
private final Map<Integer, JobMigration> migrations;
|
||||
|
||||
public JobMigrator(int lastSeenVersion, int currentVersion, @NonNull List<JobMigration> migrations) {
|
||||
this.lastSeenVersion = lastSeenVersion;
|
||||
this.currentVersion = currentVersion;
|
||||
this.migrations = new HashMap<>();
|
||||
|
||||
if (migrations.size() != currentVersion - 1) {
|
||||
throw new AssertionError("You must have a migration for every version!");
|
||||
}
|
||||
|
||||
for (int i = 0; i < migrations.size(); i++) {
|
||||
JobMigration migration = migrations.get(i);
|
||||
|
||||
if (migration.getEndVersion() != i + 2) {
|
||||
throw new AssertionError("Missing migration for version " + (i + 2) + "!");
|
||||
}
|
||||
|
||||
this.migrations.put(migration.getEndVersion(), migrations.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The version that has been migrated to.
|
||||
*/
|
||||
int migrate(@NonNull JobStorage jobStorage, @NonNull Data.Serializer dataSerializer) {
|
||||
List<JobSpec> jobSpecs = jobStorage.getAllJobSpecs();
|
||||
|
||||
for (int i = lastSeenVersion; i < currentVersion; i++) {
|
||||
Log.i(TAG, "Migrating from " + i + " to " + (i + 1));
|
||||
|
||||
ListIterator<JobSpec> iter = jobSpecs.listIterator();
|
||||
JobMigration migration = migrations.get(i + 1);
|
||||
|
||||
assert migration != null;
|
||||
|
||||
while (iter.hasNext()) {
|
||||
JobSpec jobSpec = iter.next();
|
||||
Data data = dataSerializer.deserialize(jobSpec.getSerializedData());
|
||||
JobData originalJobData = new JobData(jobSpec.getFactoryKey(), jobSpec.getQueueKey(), data);
|
||||
JobData updatedJobData = migration.migrate(originalJobData);
|
||||
JobSpec updatedJobSpec = new JobSpec(jobSpec.getId(),
|
||||
updatedJobData.getFactoryKey(),
|
||||
updatedJobData.getQueueKey(),
|
||||
jobSpec.getCreateTime(),
|
||||
jobSpec.getNextRunAttemptTime(),
|
||||
jobSpec.getRunAttempt(),
|
||||
jobSpec.getMaxAttempts(),
|
||||
jobSpec.getMaxBackoff(),
|
||||
jobSpec.getLifespan(),
|
||||
jobSpec.getMaxInstances(),
|
||||
dataSerializer.serialize(updatedJobData.getData()),
|
||||
jobSpec.isRunning());
|
||||
|
||||
iter.set(updatedJobSpec);
|
||||
}
|
||||
}
|
||||
|
||||
jobStorage.updateJobs(jobSpecs);
|
||||
|
||||
return currentVersion;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.PowerManager;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.WakeLockUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A thread that constantly checks for available {@link Job}s owned by the {@link JobController}.
|
||||
* When one is available, this class will execute it and call the appropriate methods on
|
||||
* {@link JobController} based on the result.
|
||||
*
|
||||
* {@link JobRunner} and {@link JobController} were written such that you should be able to have
|
||||
* N concurrent {@link JobRunner}s operating over the same {@link JobController}.
|
||||
*/
|
||||
class JobRunner extends Thread {
|
||||
|
||||
private static final String TAG = JobRunner.class.getSimpleName();
|
||||
|
||||
private static long WAKE_LOCK_TIMEOUT = TimeUnit.MINUTES.toMillis(10);
|
||||
|
||||
private final Application application;
|
||||
private final int id;
|
||||
private final JobController jobController;
|
||||
|
||||
JobRunner(@NonNull Application application, int id, @NonNull JobController jobController) {
|
||||
super("signal-JobRunner-" + id);
|
||||
|
||||
this.application = application;
|
||||
this.id = id;
|
||||
this.jobController = jobController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void run() {
|
||||
//noinspection InfiniteLoopStatement
|
||||
while (true) {
|
||||
Job job = jobController.pullNextEligibleJobForExecution();
|
||||
Job.Result result = run(job);
|
||||
|
||||
jobController.onJobFinished(job);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
jobController.onSuccess(job);
|
||||
} else if (result.isRetry()) {
|
||||
jobController.onRetry(job);
|
||||
job.onRetry();
|
||||
} else if (result.isFailure()) {
|
||||
List<Job> dependents = jobController.onFailure(job);
|
||||
job.onCanceled();
|
||||
Stream.of(dependents).forEach(Job::onCanceled);
|
||||
|
||||
if (result.getException() != null) {
|
||||
throw result.getException();
|
||||
}
|
||||
} else {
|
||||
throw new AssertionError("Invalid job result!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Job.Result run(@NonNull Job job) {
|
||||
Log.i(TAG, JobLogger.format(job, String.valueOf(id), "Running job."));
|
||||
|
||||
if (isJobExpired(job)) {
|
||||
Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing after surpassing its lifespan."));
|
||||
return Job.Result.failure();
|
||||
}
|
||||
|
||||
Job.Result result = null;
|
||||
PowerManager.WakeLock wakeLock = null;
|
||||
|
||||
try {
|
||||
wakeLock = WakeLockUtil.acquire(application, PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TIMEOUT, job.getId());
|
||||
result = job.run();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing due to an unexpected exception."), e);
|
||||
return Job.Result.failure();
|
||||
} finally {
|
||||
if (wakeLock != null) {
|
||||
WakeLockUtil.release(wakeLock, job.getId());
|
||||
}
|
||||
}
|
||||
|
||||
printResult(job, result);
|
||||
|
||||
if (result.isRetry() &&
|
||||
job.getRunAttempt() + 1 >= job.getParameters().getMaxAttempts() &&
|
||||
job.getParameters().getMaxAttempts() != Job.Parameters.UNLIMITED)
|
||||
{
|
||||
Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing after surpassing its max number of attempts."));
|
||||
return Job.Result.failure();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isJobExpired(@NonNull Job job) {
|
||||
long expirationTime = job.getParameters().getCreateTime() + job.getParameters().getLifespan();
|
||||
|
||||
if (expirationTime < 0) {
|
||||
expirationTime = Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
return job.getParameters().getLifespan() != Job.Parameters.IMMORTAL && expirationTime <= System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private void printResult(@NonNull Job job, @NonNull Job.Result result) {
|
||||
if (result.getException() != null) {
|
||||
Log.e(TAG, JobLogger.format(job, String.valueOf(id), "Job failed with a fatal exception. Crash imminent."));
|
||||
} else if (result.isFailure()) {
|
||||
Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Job failed."));
|
||||
} else {
|
||||
Log.i(TAG, JobLogger.format(job, String.valueOf(id), "Job finished with result: " + result));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.job.JobInfo;
|
||||
import android.app.job.JobParameters;
|
||||
import android.app.job.JobScheduler;
|
||||
import android.app.job.JobService;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RequiresApi(26)
|
||||
public class JobSchedulerScheduler implements Scheduler {
|
||||
|
||||
private static final String TAG = JobSchedulerScheduler.class.getSimpleName();
|
||||
|
||||
private static final String PREF_NAME = "JobSchedulerScheduler_prefs";
|
||||
private static final String PREF_NEXT_ID = "pref_next_id";
|
||||
|
||||
private static final int MAX_ID = 20;
|
||||
|
||||
private final Application application;
|
||||
|
||||
JobSchedulerScheduler(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@RequiresApi(26)
|
||||
@Override
|
||||
public void schedule(long delay, @NonNull List<Constraint> constraints) {
|
||||
JobInfo.Builder jobInfoBuilder = new JobInfo.Builder(getNextId(), new ComponentName(application, SystemService.class))
|
||||
.setMinimumLatency(delay)
|
||||
.setPersisted(true);
|
||||
|
||||
for (Constraint constraint : constraints) {
|
||||
constraint.applyToJobInfo(jobInfoBuilder);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Scheduling a run in " + delay + " ms.");
|
||||
JobScheduler jobScheduler = application.getSystemService(JobScheduler.class);
|
||||
jobScheduler.schedule(jobInfoBuilder.build());
|
||||
}
|
||||
|
||||
private int getNextId() {
|
||||
SharedPreferences prefs = application.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
||||
int returnedId = prefs.getInt(PREF_NEXT_ID, 0);
|
||||
int nextId = returnedId + 1 > MAX_ID ? 0 : returnedId + 1;
|
||||
|
||||
prefs.edit().putInt(PREF_NEXT_ID, nextId).apply();
|
||||
|
||||
return returnedId;
|
||||
}
|
||||
|
||||
@RequiresApi(api = 26)
|
||||
public static class SystemService extends JobService {
|
||||
|
||||
@Override
|
||||
public boolean onStartJob(JobParameters params) {
|
||||
Log.d(TAG, "onStartJob()");
|
||||
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
|
||||
jobManager.addOnEmptyQueueListener(new JobManager.EmptyQueueListener() {
|
||||
@Override
|
||||
public void onQueueEmpty() {
|
||||
jobManager.removeOnEmptyQueueListener(this);
|
||||
jobFinished(params, false);
|
||||
Log.d(TAG, "jobFinished()");
|
||||
}
|
||||
});
|
||||
|
||||
jobManager.wakeUp();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onStopJob(JobParameters params) {
|
||||
Log.d(TAG, "onStopJob()");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.util.LRUCache;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Tracks the state of {@link Job}s and allows callers to listen to changes.
|
||||
*/
|
||||
public class JobTracker {
|
||||
|
||||
private final Map<String, TrackingState> trackingStates;
|
||||
private final Executor listenerExecutor;
|
||||
|
||||
JobTracker() {
|
||||
this.trackingStates = new LRUCache<>(1000);
|
||||
this.listenerExecutor = SignalExecutors.BOUNDED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener to subscribe to job state updates. Listeners will be invoked on an arbitrary
|
||||
* background thread. You must eventually call {@link #removeListener(JobListener)} to avoid
|
||||
* memory leaks.
|
||||
*/
|
||||
synchronized void addListener(@NonNull String id, @NonNull JobListener jobListener) {
|
||||
TrackingState state = getOrCreateTrackingState(id);
|
||||
JobState currentJobState = state.getJobState();
|
||||
|
||||
state.addListener(jobListener);
|
||||
|
||||
if (currentJobState != null) {
|
||||
listenerExecutor.execute(() -> jobListener.onStateChanged(currentJobState));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe the provided listener from all job updates.
|
||||
*/
|
||||
synchronized void removeListener(@NonNull JobListener jobListener) {
|
||||
Collection<TrackingState> allTrackingState = trackingStates.values();
|
||||
|
||||
for (TrackingState state : allTrackingState) {
|
||||
state.removeListener(jobListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state of a job with the associated ID.
|
||||
*/
|
||||
synchronized void onStateChange(@NonNull String id, @NonNull JobState jobState) {
|
||||
TrackingState trackingState = getOrCreateTrackingState(id);
|
||||
trackingState.setJobState(jobState);
|
||||
|
||||
for (JobListener listener : trackingState.getListeners()) {
|
||||
listenerExecutor.execute(() -> listener.onStateChanged(jobState));
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull TrackingState getOrCreateTrackingState(@NonNull String id) {
|
||||
TrackingState state = trackingStates.get(id);
|
||||
|
||||
if (state == null) {
|
||||
state = new TrackingState();
|
||||
}
|
||||
|
||||
trackingStates.put(id, state);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
public interface JobListener {
|
||||
void onStateChanged(@NonNull JobState jobState);
|
||||
}
|
||||
|
||||
public enum JobState {
|
||||
PENDING(false), RUNNING(false), SUCCESS(true), FAILURE(true), IGNORED(true);
|
||||
|
||||
private final boolean complete;
|
||||
|
||||
JobState(boolean complete) {
|
||||
this.complete = complete;
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return complete;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TrackingState {
|
||||
private JobState jobState;
|
||||
|
||||
private final CopyOnWriteArraySet<JobListener> listeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
void addListener(@NonNull JobListener jobListener) {
|
||||
listeners.add(jobListener);
|
||||
}
|
||||
|
||||
void removeListener(@NonNull JobListener jobListener) {
|
||||
listeners.remove(jobListener);
|
||||
}
|
||||
|
||||
@NonNull Collection<JobListener> getListeners() {
|
||||
return listeners;
|
||||
}
|
||||
|
||||
void setJobState(@NonNull JobState jobState) {
|
||||
this.jobState = jobState;
|
||||
}
|
||||
|
||||
@Nullable JobState getJobState() {
|
||||
return jobState;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Service that keeps the application in memory while the app is closed.
|
||||
*
|
||||
* Important: Should only be used on API < 26.
|
||||
*/
|
||||
public class KeepAliveService extends Service {
|
||||
|
||||
@Override
|
||||
public @Nullable IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
return START_STICKY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface Scheduler {
|
||||
void schedule(long delay, @NonNull List<Constraint> constraints);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.job.JobInfo;
|
||||
import android.telephony.ServiceState;
|
||||
import android.telephony.TelephonyManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Constraint;
|
||||
import org.thoughtcrime.securesms.sms.TelephonyServiceState;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
|
||||
public class CellServiceConstraint implements Constraint {
|
||||
|
||||
public static final String KEY = "CellServiceConstraint";
|
||||
|
||||
private final Application application;
|
||||
|
||||
public CellServiceConstraint(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMet() {
|
||||
return CellServiceConstraintObserver.getInstance(application).hasService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder) {
|
||||
}
|
||||
|
||||
public static final class Factory implements Constraint.Factory<CellServiceConstraint> {
|
||||
|
||||
private final Application application;
|
||||
|
||||
public Factory(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellServiceConstraint create() {
|
||||
return new CellServiceConstraint(application);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.ServiceState;
|
||||
import android.telephony.TelephonyManager;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
|
||||
|
||||
public class CellServiceConstraintObserver implements ConstraintObserver {
|
||||
|
||||
private static final String REASON = CellServiceConstraintObserver.class.getSimpleName();
|
||||
|
||||
private volatile Notifier notifier;
|
||||
private volatile ServiceState lastKnownState;
|
||||
|
||||
private static CellServiceConstraintObserver instance;
|
||||
|
||||
public static synchronized CellServiceConstraintObserver getInstance(@NonNull Application application) {
|
||||
if (instance == null) {
|
||||
instance = new CellServiceConstraintObserver(application);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private CellServiceConstraintObserver(@NonNull Application application) {
|
||||
TelephonyManager telephonyManager = (TelephonyManager) application.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
ServiceStateListener serviceStateListener = new ServiceStateListener();
|
||||
|
||||
telephonyManager.listen(serviceStateListener, PhoneStateListener.LISTEN_SERVICE_STATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(@NonNull Notifier notifier) {
|
||||
this.notifier = notifier;
|
||||
}
|
||||
|
||||
public boolean hasService() {
|
||||
return lastKnownState != null && lastKnownState.getState() == ServiceState.STATE_IN_SERVICE;
|
||||
}
|
||||
|
||||
private class ServiceStateListener extends PhoneStateListener {
|
||||
@Override
|
||||
public void onServiceStateChanged(ServiceState serviceState) {
|
||||
lastKnownState = serviceState;
|
||||
|
||||
if (serviceState.getState() == ServiceState.STATE_IN_SERVICE && notifier != null) {
|
||||
notifier.onConstraintMet(REASON);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.ExecutorFactory;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class DefaultExecutorFactory implements ExecutorFactory {
|
||||
@Override
|
||||
public @NonNull ExecutorService newSingleThreadExecutor(@NonNull String name) {
|
||||
return Executors.newSingleThreadExecutor(r -> new Thread(r, name));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class JsonDataSerializer implements Data.Serializer {
|
||||
|
||||
private static final String TAG = Log.tag(JsonDataSerializer.class);
|
||||
|
||||
@Override
|
||||
public @NonNull String serialize(@NonNull Data data) {
|
||||
try {
|
||||
return JsonUtils.toJson(data);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to serialize to JSON.", e);
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data deserialize(@NonNull String serialized) {
|
||||
try {
|
||||
return JsonUtils.fromJson(serialized, Data.class);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to deserialize JSON.", e);
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.job.JobInfo;
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Constraint;
|
||||
|
||||
public class NetworkConstraint implements Constraint {
|
||||
|
||||
public static final String KEY = "NetworkConstraint";
|
||||
|
||||
private final Application application;
|
||||
|
||||
private NetworkConstraint(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMet() {
|
||||
ConnectivityManager connectivityManager = (ConnectivityManager) application.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
|
||||
|
||||
return activeNetworkInfo != null && activeNetworkInfo.isConnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@RequiresApi(26)
|
||||
@Override
|
||||
public void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder) {
|
||||
jobInfoBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
|
||||
}
|
||||
|
||||
public static final class Factory implements Constraint.Factory<NetworkConstraint> {
|
||||
|
||||
private final Application application;
|
||||
|
||||
public Factory(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NetworkConstraint create() {
|
||||
return new NetworkConstraint(application);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
|
||||
|
||||
public class NetworkConstraintObserver implements ConstraintObserver {
|
||||
|
||||
private static final String REASON = NetworkConstraintObserver.class.getSimpleName();
|
||||
|
||||
private final Application application;
|
||||
|
||||
public NetworkConstraintObserver(Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(@NonNull Notifier notifier) {
|
||||
application.registerReceiver(new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
NetworkConstraint constraint = new NetworkConstraint.Factory(application).create();
|
||||
|
||||
if (constraint.isMet()) {
|
||||
notifier.onConstraintMet(REASON);
|
||||
}
|
||||
}
|
||||
}, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.job.JobInfo;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Constraint;
|
||||
|
||||
public class NetworkOrCellServiceConstraint implements Constraint {
|
||||
|
||||
public static final String KEY = "NetworkOrCellServiceConstraint";
|
||||
|
||||
private final NetworkConstraint networkConstraint;
|
||||
private final CellServiceConstraint serviceConstraint;
|
||||
|
||||
public NetworkOrCellServiceConstraint(@NonNull Application application) {
|
||||
networkConstraint = new NetworkConstraint.Factory(application).create();
|
||||
serviceConstraint = new CellServiceConstraint.Factory(application).create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMet() {
|
||||
return networkConstraint.isMet() || serviceConstraint.isMet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder) {
|
||||
}
|
||||
|
||||
public static class Factory implements Constraint.Factory<NetworkOrCellServiceConstraint> {
|
||||
|
||||
private final Application application;
|
||||
|
||||
public Factory(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NetworkOrCellServiceConstraint create() {
|
||||
return new NetworkOrCellServiceConstraint(application);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.job.JobInfo;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Constraint;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class SqlCipherMigrationConstraint implements Constraint {
|
||||
|
||||
public static final String KEY = "SqlCipherMigrationConstraint";
|
||||
|
||||
private final Application application;
|
||||
|
||||
private SqlCipherMigrationConstraint(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMet() {
|
||||
return !TextSecurePreferences.getNeedsSqlCipherMigration(application);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder) {
|
||||
}
|
||||
|
||||
public static final class Factory implements Constraint.Factory<SqlCipherMigrationConstraint> {
|
||||
|
||||
private final Application application;
|
||||
|
||||
public Factory(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlCipherMigrationConstraint create() {
|
||||
return new SqlCipherMigrationConstraint(application);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
|
||||
|
||||
public class SqlCipherMigrationConstraintObserver implements ConstraintObserver {
|
||||
|
||||
private static final String REASON = SqlCipherMigrationConstraintObserver.class.getSimpleName();
|
||||
|
||||
private Notifier notifier;
|
||||
|
||||
public SqlCipherMigrationConstraintObserver() {
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(@NonNull Notifier notifier) {
|
||||
this.notifier = notifier;
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(SqlCipherNeedsMigrationEvent event) {
|
||||
if (notifier != null) notifier.onConstraintMet(REASON);
|
||||
}
|
||||
|
||||
public static class SqlCipherNeedsMigrationEvent {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.migrations;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobMigration;
|
||||
|
||||
/**
|
||||
* Fixes things that went wrong in {@link RecipientIdJobMigration}. In particular, some jobs didn't
|
||||
* have some necessary data fields carried over. Thankfully they're relatively non-critical, so
|
||||
* we'll just swap them out with failing jobs if they're missing something.
|
||||
*/
|
||||
public class RecipientIdFollowUpJobMigration extends JobMigration {
|
||||
|
||||
public RecipientIdFollowUpJobMigration() {
|
||||
this(3);
|
||||
}
|
||||
|
||||
RecipientIdFollowUpJobMigration(int endVersion) {
|
||||
super(endVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull JobData migrate(@NonNull JobData jobData) {
|
||||
switch(jobData.getFactoryKey()) {
|
||||
case "RequestGroupInfoJob": return migrateRequestGroupInfoJob(jobData);
|
||||
case "SendDeliveryReceiptJob": return migrateSendDeliveryReceiptJob(jobData);
|
||||
default:
|
||||
return jobData;
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull JobData migrateRequestGroupInfoJob(@NonNull JobData jobData) {
|
||||
if (!jobData.getData().hasString("source") || !jobData.getData().hasString("group_id")) {
|
||||
return failingJobData();
|
||||
} else {
|
||||
return jobData;
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull JobData migrateSendDeliveryReceiptJob(@NonNull JobData jobData) {
|
||||
if (!jobData.getData().hasString("recipient") ||
|
||||
!jobData.getData().hasLong("message_id") ||
|
||||
!jobData.getData().hasLong("timestamp"))
|
||||
{
|
||||
return failingJobData();
|
||||
} else {
|
||||
return jobData;
|
||||
}
|
||||
}
|
||||
|
||||
private static JobData failingJobData() {
|
||||
return new JobData("FailingJob", null, new Data.Builder().build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.migrations;
|
||||
|
||||
/**
|
||||
* Unfortunately there was a bug in {@link RecipientIdFollowUpJobMigration} that requires it to be
|
||||
* run again.
|
||||
*/
|
||||
public class RecipientIdFollowUpJobMigration2 extends RecipientIdFollowUpJobMigration {
|
||||
public RecipientIdFollowUpJobMigration2() {
|
||||
super(4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.migrations;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobMigration;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
public class RecipientIdJobMigration extends JobMigration {
|
||||
|
||||
private final Application application;
|
||||
|
||||
public RecipientIdJobMigration(@NonNull Application application) {
|
||||
super(2);
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull JobData migrate(@NonNull JobData jobData) {
|
||||
switch(jobData.getFactoryKey()) {
|
||||
case "MultiDeviceContactUpdateJob": return migrateMultiDeviceContactUpdateJob(jobData);
|
||||
case "MultiDeviceRevealUpdateJob": return migrateMultiDeviceViewOnceOpenJob(jobData);
|
||||
case "RequestGroupInfoJob": return migrateRequestGroupInfoJob(jobData);
|
||||
case "SendDeliveryReceiptJob": return migrateSendDeliveryReceiptJob(jobData);
|
||||
case "MultiDeviceVerifiedUpdateJob": return migrateMultiDeviceVerifiedUpdateJob(jobData);
|
||||
case "RetrieveProfileJob": return migrateRetrieveProfileJob(jobData);
|
||||
case "PushGroupSendJob": return migratePushGroupSendJob(jobData);
|
||||
case "PushGroupUpdateJob": return migratePushGroupUpdateJob(jobData);
|
||||
case "DirectoryRefreshJob": return migrateDirectoryRefreshJob(jobData);
|
||||
case "RetrieveProfileAvatarJob": return migrateRetrieveProfileAvatarJob(jobData);
|
||||
case "MultiDeviceReadUpdateJob": return migrateMultiDeviceReadUpdateJob(jobData);
|
||||
case "PushTextSendJob": return migratePushTextSendJob(jobData);
|
||||
case "PushMediaSendJob": return migratePushMediaSendJob(jobData);
|
||||
case "SmsSendJob": return migrateSmsSendJob(jobData);
|
||||
default: return jobData;
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull JobData migrateMultiDeviceContactUpdateJob(@NonNull JobData jobData) {
|
||||
String address = jobData.getData().hasString("address") ? jobData.getData().getString("address") : null;
|
||||
Data updatedData = new Data.Builder().putString("recipient", address != null ? Recipient.external(application, address).getId().serialize() : null)
|
||||
.putBoolean("force_sync", jobData.getData().getBoolean("force_sync"))
|
||||
.build();
|
||||
|
||||
return jobData.withData(updatedData);
|
||||
}
|
||||
|
||||
private @NonNull JobData migrateMultiDeviceViewOnceOpenJob(@NonNull JobData jobData) {
|
||||
try {
|
||||
String rawOld = jobData.getData().getString("message_id");
|
||||
OldSerializableSyncMessageId old = JsonUtils.fromJson(rawOld, OldSerializableSyncMessageId.class);
|
||||
Recipient recipient = Recipient.external(application, old.sender);
|
||||
NewSerializableSyncMessageId updated = new NewSerializableSyncMessageId(recipient.getId().serialize(), old.timestamp);
|
||||
String rawUpdated = JsonUtils.toJson(updated);
|
||||
Data updatedData = new Data.Builder().putString("message_id", rawUpdated).build();
|
||||
|
||||
return jobData.withData(updatedData);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull JobData migrateRequestGroupInfoJob(@NonNull JobData jobData) {
|
||||
String address = jobData.getData().getString("source");
|
||||
Recipient recipient = Recipient.external(application, address);
|
||||
Data updatedData = new Data.Builder().putString("source", recipient.getId().serialize())
|
||||
.putString("group_id", jobData.getData().getString("group_id"))
|
||||
.build();
|
||||
|
||||
return jobData.withData(updatedData);
|
||||
}
|
||||
|
||||
private @NonNull JobData migrateSendDeliveryReceiptJob(@NonNull JobData jobData) {
|
||||
String address = jobData.getData().getString("address");
|
||||
Recipient recipient = Recipient.external(application, address);
|
||||
Data updatedData = new Data.Builder().putString("recipient", recipient.getId().serialize())
|
||||
.putLong("message_id", jobData.getData().getLong("message_id"))
|
||||
.putLong("timestamp", jobData.getData().getLong("timestamp"))
|
||||
.build();
|
||||
|
||||
return jobData.withData(updatedData);
|
||||
}
|
||||
|
||||
private @NonNull JobData migrateMultiDeviceVerifiedUpdateJob(@NonNull JobData jobData) {
|
||||
String address = jobData.getData().getString("destination");
|
||||
Recipient recipient = Recipient.external(application, address);
|
||||
Data updatedData = new Data.Builder().putString("destination", recipient.getId().serialize())
|
||||
.putString("identity_key", jobData.getData().getString("identity_key"))
|
||||
.putInt("verified_status", jobData.getData().getInt("verified_status"))
|
||||
.putLong("timestamp", jobData.getData().getLong("timestamp"))
|
||||
.build();
|
||||
|
||||
return jobData.withData(updatedData);
|
||||
}
|
||||
|
||||
private @NonNull JobData migrateRetrieveProfileJob(@NonNull JobData jobData) {
|
||||
String address = jobData.getData().getString("address");
|
||||
Recipient recipient = Recipient.external(application, address);
|
||||
Data updatedData = new Data.Builder().putString("recipient", recipient.getId().serialize()).build();
|
||||
|
||||
return jobData.withData(updatedData);
|
||||
}
|
||||
|
||||
private @NonNull JobData migratePushGroupSendJob(@NonNull JobData jobData) {
|
||||
// noinspection ConstantConditions
|
||||
Recipient queueRecipient = Recipient.external(application, jobData.getQueueKey());
|
||||
String address = jobData.getData().hasString("filter_address") ? jobData.getData().getString("filter_address") : null;
|
||||
RecipientId recipientId = address != null ? Recipient.external(application, address).getId() : null;
|
||||
Data updatedData = new Data.Builder().putString("filter_recipient", recipientId != null ? recipientId.serialize() : null)
|
||||
.putLong("message_id", jobData.getData().getLong("message_id"))
|
||||
.build();
|
||||
|
||||
return jobData.withQueueKey(queueRecipient.getId().toQueueKey())
|
||||
.withData(updatedData);
|
||||
}
|
||||
|
||||
private @NonNull JobData migratePushGroupUpdateJob(@NonNull JobData jobData) {
|
||||
String address = jobData.getData().getString("source");
|
||||
Recipient recipient = Recipient.external(application, address);
|
||||
Data updatedData = new Data.Builder().putString("source", recipient.getId().serialize())
|
||||
.putString("group_id", jobData.getData().getString("group_id"))
|
||||
.build();
|
||||
|
||||
return jobData.withData(updatedData);
|
||||
}
|
||||
|
||||
private @NonNull JobData migrateDirectoryRefreshJob(@NonNull JobData jobData) {
|
||||
String address = jobData.getData().hasString("address") ? jobData.getData().getString("address") : null;
|
||||
Recipient recipient = address != null ? Recipient.external(application, address) : null;
|
||||
Data updatedData = new Data.Builder().putString("recipient", recipient != null ? recipient.getId().serialize() : null)
|
||||
.putBoolean("notify_of_new_users", jobData.getData().getBoolean("notify_of_new_users"))
|
||||
.build();
|
||||
|
||||
return jobData.withData(updatedData);
|
||||
}
|
||||
|
||||
private @NonNull JobData migrateRetrieveProfileAvatarJob(@NonNull JobData jobData) {
|
||||
//noinspection ConstantConditions
|
||||
String queueAddress = jobData.getQueueKey().substring("RetrieveProfileAvatarJob".length());
|
||||
Recipient queueRecipient = Recipient.external(application, queueAddress);
|
||||
String address = jobData.getData().getString("address");
|
||||
Recipient recipient = Recipient.external(application, address);
|
||||
Data updatedData = new Data.Builder().putString("recipient", recipient.getId().serialize())
|
||||
.putString("profile_avatar", jobData.getData().getString("profile_avatar"))
|
||||
.build();
|
||||
|
||||
return jobData.withQueueKey("RetrieveProfileAvatarJob::" + queueRecipient.getId().toQueueKey())
|
||||
.withData(updatedData);
|
||||
}
|
||||
|
||||
private @NonNull JobData migrateMultiDeviceReadUpdateJob(@NonNull JobData jobData) {
|
||||
try {
|
||||
String[] rawOld = jobData.getData().getStringArray("message_ids");
|
||||
String[] rawUpdated = new String[rawOld.length];
|
||||
|
||||
for (int i = 0; i < rawOld.length; i++) {
|
||||
OldSerializableSyncMessageId old = JsonUtils.fromJson(rawOld[i], OldSerializableSyncMessageId.class);
|
||||
Recipient recipient = Recipient.external(application, old.sender);
|
||||
NewSerializableSyncMessageId updated = new NewSerializableSyncMessageId(recipient.getId().serialize(), old.timestamp);
|
||||
|
||||
rawUpdated[i] = JsonUtils.toJson(updated);
|
||||
}
|
||||
|
||||
Data updatedData = new Data.Builder().putStringArray("message_ids", rawUpdated).build();
|
||||
|
||||
return jobData.withData(updatedData);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull JobData migratePushTextSendJob(@NonNull JobData jobData) {
|
||||
//noinspection ConstantConditions
|
||||
Recipient recipient = Recipient.external(application, jobData.getQueueKey());
|
||||
return jobData.withQueueKey(recipient.getId().toQueueKey());
|
||||
}
|
||||
|
||||
private @NonNull JobData migratePushMediaSendJob(@NonNull JobData jobData) {
|
||||
//noinspection ConstantConditions
|
||||
Recipient recipient = Recipient.external(application, jobData.getQueueKey());
|
||||
return jobData.withQueueKey(recipient.getId().toQueueKey());
|
||||
}
|
||||
|
||||
private @NonNull JobData migrateSmsSendJob(@NonNull JobData jobData) {
|
||||
//noinspection ConstantConditions
|
||||
if (jobData.getQueueKey() != null) {
|
||||
Recipient recipient = Recipient.external(application, jobData.getQueueKey());
|
||||
return jobData.withQueueKey(recipient.getId().toQueueKey());
|
||||
} else {
|
||||
return jobData;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class OldSerializableSyncMessageId implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonProperty
|
||||
private final String sender;
|
||||
@JsonProperty
|
||||
private final long timestamp;
|
||||
|
||||
OldSerializableSyncMessageId(@JsonProperty("sender") String sender, @JsonProperty("timestamp") long timestamp) {
|
||||
this.sender = sender;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class NewSerializableSyncMessageId implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonProperty
|
||||
private final String recipientId;
|
||||
@JsonProperty
|
||||
private final long timestamp;
|
||||
|
||||
NewSerializableSyncMessageId(@JsonProperty("recipientId") String recipientId, @JsonProperty("timestamp") long timestamp) {
|
||||
this.recipientId = recipientId;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ConstraintSpec {
|
||||
|
||||
private final String jobSpecId;
|
||||
private final String factoryKey;
|
||||
|
||||
public ConstraintSpec(@NonNull String jobSpecId, @NonNull String factoryKey) {
|
||||
this.jobSpecId = jobSpecId;
|
||||
this.factoryKey = factoryKey;
|
||||
}
|
||||
|
||||
public String getJobSpecId() {
|
||||
return jobSpecId;
|
||||
}
|
||||
|
||||
public String getFactoryKey() {
|
||||
return factoryKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ConstraintSpec that = (ConstraintSpec) o;
|
||||
return Objects.equals(jobSpecId, that.jobSpecId) &&
|
||||
Objects.equals(factoryKey, that.factoryKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(jobSpecId, factoryKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format("jobSpecId: JOB::%s | factoryKey: %s", jobSpecId, factoryKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class DependencySpec {
|
||||
|
||||
private final String jobId;
|
||||
private final String dependsOnJobId;
|
||||
|
||||
public DependencySpec(@NonNull String jobId, @NonNull String dependsOnJobId) {
|
||||
this.jobId = jobId;
|
||||
this.dependsOnJobId = dependsOnJobId;
|
||||
}
|
||||
|
||||
public @NonNull String getJobId() {
|
||||
return jobId;
|
||||
}
|
||||
|
||||
public @NonNull String getDependsOnJobId() {
|
||||
return dependsOnJobId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
DependencySpec that = (DependencySpec) o;
|
||||
return Objects.equals(jobId, that.jobId) &&
|
||||
Objects.equals(dependsOnJobId, that.dependsOnJobId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(jobId, dependsOnJobId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format("jobSpecId: JOB::%s | dependsOnJobSpecId: JOB::%s", jobId, dependsOnJobId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class FullSpec {
|
||||
|
||||
private final JobSpec jobSpec;
|
||||
private final List<ConstraintSpec> constraintSpecs;
|
||||
private final List<DependencySpec> dependencySpecs;
|
||||
|
||||
public FullSpec(@NonNull JobSpec jobSpec,
|
||||
@NonNull List<ConstraintSpec> constraintSpecs,
|
||||
@NonNull List<DependencySpec> dependencySpecs)
|
||||
{
|
||||
this.jobSpec = jobSpec;
|
||||
this.constraintSpecs = constraintSpecs;
|
||||
this.dependencySpecs = dependencySpecs;
|
||||
}
|
||||
|
||||
public @NonNull JobSpec getJobSpec() {
|
||||
return jobSpec;
|
||||
}
|
||||
|
||||
public @NonNull List<ConstraintSpec> getConstraintSpecs() {
|
||||
return constraintSpecs;
|
||||
}
|
||||
|
||||
public @NonNull List<DependencySpec> getDependencySpecs() {
|
||||
return dependencySpecs;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
FullSpec fullSpec = (FullSpec) o;
|
||||
return Objects.equals(jobSpec, fullSpec.jobSpec) &&
|
||||
Objects.equals(constraintSpecs, fullSpec.constraintSpecs) &&
|
||||
Objects.equals(dependencySpecs, fullSpec.dependencySpecs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(jobSpec, constraintSpecs, dependencySpecs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class JobSpec {
|
||||
|
||||
private final String id;
|
||||
private final String factoryKey;
|
||||
private final String queueKey;
|
||||
private final long createTime;
|
||||
private final long nextRunAttemptTime;
|
||||
private final int runAttempt;
|
||||
private final int maxAttempts;
|
||||
private final long maxBackoff;
|
||||
private final long lifespan;
|
||||
private final int maxInstances;
|
||||
private final String serializedData;
|
||||
private final boolean isRunning;
|
||||
|
||||
public JobSpec(@NonNull String id,
|
||||
@NonNull String factoryKey,
|
||||
@Nullable String queueKey,
|
||||
long createTime,
|
||||
long nextRunAttemptTime,
|
||||
int runAttempt,
|
||||
int maxAttempts,
|
||||
long maxBackoff,
|
||||
long lifespan,
|
||||
int maxInstances,
|
||||
@NonNull String serializedData,
|
||||
boolean isRunning)
|
||||
{
|
||||
this.id = id;
|
||||
this.factoryKey = factoryKey;
|
||||
this.queueKey = queueKey;
|
||||
this.createTime = createTime;
|
||||
this.nextRunAttemptTime = nextRunAttemptTime;
|
||||
this.maxBackoff = maxBackoff;
|
||||
this.runAttempt = runAttempt;
|
||||
this.maxAttempts = maxAttempts;
|
||||
this.lifespan = lifespan;
|
||||
this.maxInstances = maxInstances;
|
||||
this.serializedData = serializedData;
|
||||
this.isRunning = isRunning;
|
||||
}
|
||||
|
||||
public @NonNull String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public @NonNull String getFactoryKey() {
|
||||
return factoryKey;
|
||||
}
|
||||
|
||||
public @Nullable String getQueueKey() {
|
||||
return queueKey;
|
||||
}
|
||||
|
||||
public long getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public long getNextRunAttemptTime() {
|
||||
return nextRunAttemptTime;
|
||||
}
|
||||
|
||||
public int getRunAttempt() {
|
||||
return runAttempt;
|
||||
}
|
||||
|
||||
public int getMaxAttempts() {
|
||||
return maxAttempts;
|
||||
}
|
||||
|
||||
public long getMaxBackoff() {
|
||||
return maxBackoff;
|
||||
}
|
||||
|
||||
public int getMaxInstances() {
|
||||
return maxInstances;
|
||||
}
|
||||
|
||||
public long getLifespan() {
|
||||
return lifespan;
|
||||
}
|
||||
|
||||
public @NonNull String getSerializedData() {
|
||||
return serializedData;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return isRunning;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
JobSpec jobSpec = (JobSpec) o;
|
||||
return createTime == jobSpec.createTime &&
|
||||
nextRunAttemptTime == jobSpec.nextRunAttemptTime &&
|
||||
runAttempt == jobSpec.runAttempt &&
|
||||
maxAttempts == jobSpec.maxAttempts &&
|
||||
maxBackoff == jobSpec.maxBackoff &&
|
||||
lifespan == jobSpec.lifespan &&
|
||||
maxInstances == jobSpec.maxInstances &&
|
||||
isRunning == jobSpec.isRunning &&
|
||||
Objects.equals(id, jobSpec.id) &&
|
||||
Objects.equals(factoryKey, jobSpec.factoryKey) &&
|
||||
Objects.equals(queueKey, jobSpec.queueKey) &&
|
||||
Objects.equals(serializedData, jobSpec.serializedData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, lifespan, maxInstances, serializedData, isRunning);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format("id: JOB::%s | factoryKey: %s | queueKey: %s | createTime: %d | nextRunAttemptTime: %d | runAttempt: %d | maxAttempts: %d | maxBackoff: %d | maxInstances: %d | lifespan: %d | isRunning: %b | data: %s",
|
||||
id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, maxInstances, lifespan, isRunning, serializedData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface JobStorage {
|
||||
|
||||
@WorkerThread
|
||||
void init();
|
||||
|
||||
@WorkerThread
|
||||
void insertJobs(@NonNull List<FullSpec> fullSpecs);
|
||||
|
||||
@WorkerThread
|
||||
@Nullable JobSpec getJobSpec(@NonNull String id);
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<JobSpec> getAllJobSpecs();
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<JobSpec> getPendingJobsWithNoDependenciesInCreatedOrder(long currentTime);
|
||||
|
||||
@WorkerThread
|
||||
int getJobInstanceCount(@NonNull String factoryKey);
|
||||
|
||||
@WorkerThread
|
||||
void updateJobRunningState(@NonNull String id, boolean isRunning);
|
||||
|
||||
@WorkerThread
|
||||
void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime, @NonNull String serializedData);
|
||||
|
||||
@WorkerThread
|
||||
void updateAllJobsToBePending();
|
||||
|
||||
@WorkerThread
|
||||
void updateJobs(@NonNull List<JobSpec> jobSpecs);
|
||||
|
||||
@WorkerThread
|
||||
void deleteJob(@NonNull String id);
|
||||
|
||||
@WorkerThread
|
||||
void deleteJobs(@NonNull List<String> ids);
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<ConstraintSpec> getConstraintSpecs(@NonNull String jobId);
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<ConstraintSpec> getAllConstraintSpecs();
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<DependencySpec> getDependencySpecsThatDependOnJob(@NonNull String jobSpecId);
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<DependencySpec> getAllDependencySpecs();
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.workmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Takes a persisted data blob stored by WorkManager and converts it to our {@link Data} class.
|
||||
*/
|
||||
final class DataMigrator {
|
||||
|
||||
private static final String TAG = Log.tag(DataMigrator.class);
|
||||
|
||||
static final Data convert(@NonNull byte[] workManagerData) {
|
||||
Map<String, Object> values = parseWorkManagerDataMap(workManagerData);
|
||||
|
||||
Data.Builder builder = new Data.Builder();
|
||||
|
||||
for (Map.Entry<String, Object> entry : values.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
|
||||
if (value == null) {
|
||||
builder.putString(entry.getKey(), null);
|
||||
} else {
|
||||
Class type = value.getClass();
|
||||
|
||||
if (type == String.class) {
|
||||
builder.putString(entry.getKey(), (String) value);
|
||||
} else if (type == String[].class) {
|
||||
builder.putStringArray(entry.getKey(), (String[]) value);
|
||||
} else if (type == Integer.class || type == int.class) {
|
||||
builder.putInt(entry.getKey(), (int) value);
|
||||
} else if (type == Integer[].class || type == int[].class) {
|
||||
builder.putIntArray(entry.getKey(), convertToIntArray(value, type));
|
||||
} else if (type == Long.class || type == long.class) {
|
||||
builder.putLong(entry.getKey(), (long) value);
|
||||
} else if (type == Long[].class || type == long[].class) {
|
||||
builder.putLongArray(entry.getKey(), convertToLongArray(value, type));
|
||||
} else if (type == Float.class || type == float.class) {
|
||||
builder.putFloat(entry.getKey(), (float) value);
|
||||
} else if (type == Float[].class || type == float[].class) {
|
||||
builder.putFloatArray(entry.getKey(), convertToFloatArray(value, type));
|
||||
} else if (type == Double.class || type == double.class) {
|
||||
builder.putDouble(entry.getKey(), (double) value);
|
||||
} else if (type == Double[].class || type == double[].class) {
|
||||
builder.putDoubleArray(entry.getKey(), convertToDoubleArray(value, type));
|
||||
} else if (type == Boolean.class || type == boolean.class) {
|
||||
builder.putBoolean(entry.getKey(), (boolean) value);
|
||||
} else if (type == Boolean[].class || type == boolean[].class) {
|
||||
builder.putBooleanArray(entry.getKey(), convertToBooleanArray(value, type));
|
||||
} else {
|
||||
Log.w(TAG, "Encountered unexpected type '" + type + "'. Skipping.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static @NonNull Map<String, Object> parseWorkManagerDataMap(@NonNull byte[] bytes) throws IllegalStateException {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
|
||||
ObjectInputStream objectInputStream = null;
|
||||
|
||||
try {
|
||||
objectInputStream = new ObjectInputStream(inputStream);
|
||||
|
||||
for (int i = objectInputStream.readInt(); i > 0; i--) {
|
||||
map.put(objectInputStream.readUTF(), objectInputStream.readObject());
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
Log.w(TAG, "Failed to read WorkManager data.", e);
|
||||
} finally {
|
||||
try {
|
||||
inputStream.close();
|
||||
|
||||
if (objectInputStream != null) {
|
||||
objectInputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to close streams after reading WorkManager data.", e);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static int[] convertToIntArray(Object value, Class type) {
|
||||
if (type == int[].class) {
|
||||
return (int[]) value;
|
||||
}
|
||||
|
||||
Integer[] casted = (Integer[]) value;
|
||||
int[] output = new int[casted.length];
|
||||
|
||||
for (int i = 0; i < casted.length; i++) {
|
||||
output[i] = casted[i];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static long[] convertToLongArray(Object value, Class type) {
|
||||
if (type == long[].class) {
|
||||
return (long[]) value;
|
||||
}
|
||||
|
||||
Long[] casted = (Long[]) value;
|
||||
long[] output = new long[casted.length];
|
||||
|
||||
for (int i = 0; i < casted.length; i++) {
|
||||
output[i] = casted[i];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static float[] convertToFloatArray(Object value, Class type) {
|
||||
if (type == float[].class) {
|
||||
return (float[]) value;
|
||||
}
|
||||
|
||||
Float[] casted = (Float[]) value;
|
||||
float[] output = new float[casted.length];
|
||||
|
||||
for (int i = 0; i < casted.length; i++) {
|
||||
output[i] = casted[i];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static double[] convertToDoubleArray(Object value, Class type) {
|
||||
if (type == double[].class) {
|
||||
return (double[]) value;
|
||||
}
|
||||
|
||||
Double[] casted = (Double[]) value;
|
||||
double[] output = new double[casted.length];
|
||||
|
||||
for (int i = 0; i < casted.length; i++) {
|
||||
output[i] = casted[i];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static boolean[] convertToBooleanArray(Object value, Class type) {
|
||||
if (type == boolean[].class) {
|
||||
return (boolean[]) value;
|
||||
}
|
||||
|
||||
Boolean[] casted = (Boolean[]) value;
|
||||
boolean[] output = new boolean[casted.length];
|
||||
|
||||
for (int i = 0; i < casted.length; i++) {
|
||||
output[i] = casted[i];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.workmanager;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
final class WorkManagerDatabase extends SQLiteOpenHelper {
|
||||
|
||||
private static final String TAG = WorkManagerDatabase.class.getSimpleName();
|
||||
|
||||
static final String DB_NAME = "androidx.work.workdb";
|
||||
|
||||
WorkManagerDatabase(@NonNull Context context) {
|
||||
super(context, DB_NAME, null, 5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
throw new UnsupportedOperationException("We should never be creating this database, only migrating an existing one!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
// There's a chance that a user who hasn't upgraded in > 6 months could hit this onUpgrade path,
|
||||
// but we don't use any of the columns that were added in any migrations they could hit, so we
|
||||
// can ignore this.
|
||||
Log.w(TAG, "Hit onUpgrade path from " + oldVersion + " to " + newVersion);
|
||||
}
|
||||
|
||||
@NonNull List<FullSpec> getAllJobs(@NonNull Data.Serializer dataSerializer) {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
String[] columns = new String[] { "id", "worker_class_name", "input", "required_network_type"};
|
||||
List<FullSpec> fullSpecs = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = db.query("WorkSpec", columns, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String factoryName = WorkManagerFactoryMappings.getFactoryKey(cursor.getString(cursor.getColumnIndexOrThrow("worker_class_name")));
|
||||
|
||||
if (factoryName != null) {
|
||||
String id = cursor.getString(cursor.getColumnIndexOrThrow("id"));
|
||||
byte[] data = cursor.getBlob(cursor.getColumnIndexOrThrow("input"));
|
||||
|
||||
List<ConstraintSpec> constraints = new LinkedList<>();
|
||||
JobSpec jobSpec = new JobSpec(id,
|
||||
factoryName,
|
||||
getQueueKey(id),
|
||||
System.currentTimeMillis(),
|
||||
0,
|
||||
0,
|
||||
Job.Parameters.UNLIMITED,
|
||||
TimeUnit.SECONDS.toMillis(30),
|
||||
TimeUnit.DAYS.toMillis(1),
|
||||
Job.Parameters.UNLIMITED,
|
||||
dataSerializer.serialize(DataMigrator.convert(data)),
|
||||
false);
|
||||
|
||||
|
||||
|
||||
if (cursor.getInt(cursor.getColumnIndexOrThrow("required_network_type")) != 0) {
|
||||
constraints.add(new ConstraintSpec(id, NetworkConstraint.KEY));
|
||||
}
|
||||
|
||||
fullSpecs.add(new FullSpec(jobSpec, constraints, Collections.emptyList()));
|
||||
} else {
|
||||
Log.w(TAG, "Failed to find a matching factory for worker class: " + factoryName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fullSpecs;
|
||||
}
|
||||
|
||||
private @Nullable String getQueueKey(@NonNull String jobId) {
|
||||
String query = "work_spec_id = ?";
|
||||
String[] args = new String[] { jobId };
|
||||
|
||||
try (Cursor cursor = getReadableDatabase().query("WorkName", null, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getString(cursor.getColumnIndexOrThrow("name"));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.workmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
|
||||
import org.thoughtcrime.securesms.jobs.AvatarDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.CleanPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.FailingJob;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
|
||||
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.RotateSignedPreKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
|
||||
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
|
||||
import org.thoughtcrime.securesms.jobs.SmsReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.SmsSentJob;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.UpdateApkJob;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class WorkManagerFactoryMappings {
|
||||
|
||||
private static final Map<String, String> FACTORY_MAP = new HashMap<String, String>() {{
|
||||
put(AttachmentDownloadJob.class.getName(), AttachmentDownloadJob.KEY);
|
||||
put(AttachmentUploadJob.class.getName(), AttachmentUploadJob.KEY);
|
||||
put(AvatarDownloadJob.class.getName(), AvatarDownloadJob.KEY);
|
||||
put(CleanPreKeysJob.class.getName(), CleanPreKeysJob.KEY);
|
||||
put(CreateSignedPreKeyJob.class.getName(), CreateSignedPreKeyJob.KEY);
|
||||
put(DirectoryRefreshJob.class.getName(), DirectoryRefreshJob.KEY);
|
||||
put(FcmRefreshJob.class.getName(), FcmRefreshJob.KEY);
|
||||
put(LocalBackupJob.class.getName(), LocalBackupJob.KEY);
|
||||
put(MmsDownloadJob.class.getName(), MmsDownloadJob.KEY);
|
||||
put(MmsReceiveJob.class.getName(), MmsReceiveJob.KEY);
|
||||
put(MmsSendJob.class.getName(), MmsSendJob.KEY);
|
||||
put(MultiDeviceBlockedUpdateJob.class.getName(), MultiDeviceBlockedUpdateJob.KEY);
|
||||
put(MultiDeviceConfigurationUpdateJob.class.getName(), MultiDeviceConfigurationUpdateJob.KEY);
|
||||
put(MultiDeviceContactUpdateJob.class.getName(), MultiDeviceContactUpdateJob.KEY);
|
||||
put(MultiDeviceGroupUpdateJob.class.getName(), MultiDeviceGroupUpdateJob.KEY);
|
||||
put(MultiDeviceProfileKeyUpdateJob.class.getName(), MultiDeviceProfileKeyUpdateJob.KEY);
|
||||
put(MultiDeviceReadUpdateJob.class.getName(), MultiDeviceReadUpdateJob.KEY);
|
||||
put(MultiDeviceVerifiedUpdateJob.class.getName(), MultiDeviceVerifiedUpdateJob.KEY);
|
||||
put("PushContentReceiveJob", FailingJob.KEY);
|
||||
put("PushDecryptJob", PushDecryptMessageJob.KEY);
|
||||
put(PushGroupSendJob.class.getName(), PushGroupSendJob.KEY);
|
||||
put(PushGroupUpdateJob.class.getName(), PushGroupUpdateJob.KEY);
|
||||
put(PushMediaSendJob.class.getName(), PushMediaSendJob.KEY);
|
||||
put(PushNotificationReceiveJob.class.getName(), PushNotificationReceiveJob.KEY);
|
||||
put(PushTextSendJob.class.getName(), PushTextSendJob.KEY);
|
||||
put(RefreshAttributesJob.class.getName(), RefreshAttributesJob.KEY);
|
||||
put(RefreshPreKeysJob.class.getName(), RefreshPreKeysJob.KEY);
|
||||
put("RefreshUnidentifiedDeliveryAbilityJob", FailingJob.KEY);
|
||||
put(RequestGroupInfoJob.class.getName(), RequestGroupInfoJob.KEY);
|
||||
put(RetrieveProfileAvatarJob.class.getName(), RetrieveProfileAvatarJob.KEY);
|
||||
put(RetrieveProfileJob.class.getName(), RetrieveProfileJob.KEY);
|
||||
put(RotateCertificateJob.class.getName(), RotateCertificateJob.KEY);
|
||||
put(RotateProfileKeyJob.class.getName(), RotateProfileKeyJob.KEY);
|
||||
put(RotateSignedPreKeyJob.class.getName(), RotateSignedPreKeyJob.KEY);
|
||||
put(SendDeliveryReceiptJob.class.getName(), SendDeliveryReceiptJob.KEY);
|
||||
put(SendReadReceiptJob.class.getName(), SendReadReceiptJob.KEY);
|
||||
put(ServiceOutageDetectionJob.class.getName(), ServiceOutageDetectionJob.KEY);
|
||||
put(SmsReceiveJob.class.getName(), SmsReceiveJob.KEY);
|
||||
put(SmsSendJob.class.getName(), SmsSendJob.KEY);
|
||||
put(SmsSentJob.class.getName(), SmsSentJob.KEY);
|
||||
put(TrimThreadJob.class.getName(), TrimThreadJob.KEY);
|
||||
put(TypingSendJob.class.getName(), TypingSendJob.KEY);
|
||||
put(UpdateApkJob.class.getName(), UpdateApkJob.KEY);
|
||||
}};
|
||||
|
||||
public static @Nullable String getFactoryKey(@NonNull String workManagerClass) {
|
||||
return FACTORY_MAP.get(workManagerClass);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.workmanager;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WorkManagerMigrator {
|
||||
|
||||
private static final String TAG = Log.tag(WorkManagerMigrator.class);
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@WorkerThread
|
||||
public static synchronized void migrate(@NonNull Context context,
|
||||
@NonNull JobStorage jobStorage,
|
||||
@NonNull Data.Serializer dataSerializer)
|
||||
{
|
||||
long startTime = System.currentTimeMillis();
|
||||
Log.i(TAG, "Beginning WorkManager migration.");
|
||||
|
||||
WorkManagerDatabase database = new WorkManagerDatabase(context);
|
||||
List<FullSpec> fullSpecs = database.getAllJobs(dataSerializer);
|
||||
|
||||
for (FullSpec fullSpec : fullSpecs) {
|
||||
Log.i(TAG, String.format("Migrating job with key '%s' and %d constraint(s).", fullSpec.getJobSpec().getFactoryKey(), fullSpec.getConstraintSpecs().size()));
|
||||
}
|
||||
|
||||
jobStorage.insertJobs(fullSpecs);
|
||||
|
||||
context.deleteDatabase(WorkManagerDatabase.DB_NAME);
|
||||
Log.i(TAG, String.format("WorkManager migration finished. Migrated %d job(s) in %d ms.", fullSpecs.size(), System.currentTimeMillis() - startTime));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static synchronized boolean needsMigration(@NonNull Context context) {
|
||||
return context.getDatabasePath(WorkManagerDatabase.DB_NAME).exists();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user