Migrated to new JobManager.
This commit is contained in:
parent
8cf3ba424a
commit
4a3c173adb
|
@ -659,6 +659,29 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".jobmanager.JobSchedulerScheduler$SystemService"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||||
|
android:enabled="@bool/enable_job_service"
|
||||||
|
tools:targetApi="26" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".jobmanager.KeepAliveService"
|
||||||
|
android:enabled="@bool/enable_alarm_manager" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".jobmanager.AlarmManagerScheduler$RetryReceiver"
|
||||||
|
android:enabled="@bool/enable_alarm_manager" />
|
||||||
|
|
||||||
|
<!-- Probably don't need this one -->
|
||||||
|
<receiver
|
||||||
|
android:name=".jobmanager.BootReceiver"
|
||||||
|
android:enabled="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false"/>
|
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false"/>
|
||||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||||
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_W" android:value="632.0dip" />
|
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_W" android:value="632.0dip" />
|
||||||
|
|
|
@ -70,7 +70,6 @@ dependencies {
|
||||||
compile 'com.android.support:multidex:1.0.3'
|
compile 'com.android.support:multidex:1.0.3'
|
||||||
compile 'android.arch.lifecycle:extensions:1.1.1'
|
compile 'android.arch.lifecycle:extensions:1.1.1'
|
||||||
compile 'android.arch.lifecycle:common-java8:1.1.1'
|
compile 'android.arch.lifecycle:common-java8:1.1.1'
|
||||||
compile 'android.arch.work:work-runtime:1.0.0'
|
|
||||||
|
|
||||||
compile('com.google.firebase:firebase-messaging:17.3.4') {
|
compile('com.google.firebase:firebase-messaging:17.3.4') {
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||||
|
@ -177,7 +176,6 @@ dependencyVerification {
|
||||||
'com.android.support:exifinterface:bbf44e519edd6333a24a3285aa21fd00181b920b81ca8aa89a8899f03ab4d6b0',
|
'com.android.support:exifinterface:bbf44e519edd6333a24a3285aa21fd00181b920b81ca8aa89a8899f03ab4d6b0',
|
||||||
'com.android.support.constraint:constraint-layout:27b4e5c0b80d3ff8b92f4c93b3b4d3ecf16c01589f4cdf70ca7cf64cb42d8122',
|
'com.android.support.constraint:constraint-layout:27b4e5c0b80d3ff8b92f4c93b3b4d3ecf16c01589f4cdf70ca7cf64cb42d8122',
|
||||||
'com.android.support:multidex:ecf6098572e23b5155bab3b9a82b2fd1530eda6c6c157745e0f5287c66eec60c',
|
'com.android.support:multidex:ecf6098572e23b5155bab3b9a82b2fd1530eda6c6c157745e0f5287c66eec60c',
|
||||||
'android.arch.work:work-runtime:51a3c9c85bef74d60737b40bb5ab5d1f5a2b825394d26e15f19b7316ac2bff20',
|
|
||||||
'android.arch.lifecycle:extensions:429426b2feec2245ffc5e75b3b5309bedb36159cf06dc71843ae43526ac289b6',
|
'android.arch.lifecycle:extensions:429426b2feec2245ffc5e75b3b5309bedb36159cf06dc71843ae43526ac289b6',
|
||||||
'android.arch.lifecycle:common-java8:7078b5c8ccb94203df9cc2a463c69cf0021596e6cf966d78fbfd697aaafe0630',
|
'android.arch.lifecycle:common-java8:7078b5c8ccb94203df9cc2a463c69cf0021596e6cf966d78fbfd697aaafe0630',
|
||||||
'com.google.firebase:firebase-messaging:e42288e7950d7d3b033d3395a5ac9365d230da3e439a2794ec13e2ef0fbaf078',
|
'com.google.firebase:firebase-messaging:e42288e7950d7d3b033d3395a5ac9365d230da3e439a2794ec13e2ef0fbaf078',
|
||||||
|
@ -223,7 +221,6 @@ dependencyVerification {
|
||||||
'com.android.support:support-fragment:3772fc738ada86824ba1a4b3f197c3dbd67b7ddcfe2c9db1de95ef2e3487a915',
|
'com.android.support:support-fragment:3772fc738ada86824ba1a4b3f197c3dbd67b7ddcfe2c9db1de95ef2e3487a915',
|
||||||
'com.android.support:animated-vector-drawable:271ecbc906cda8dcd9e655ba0473129c3408a4189c806f616c378e6fd18fb3b7',
|
'com.android.support:animated-vector-drawable:271ecbc906cda8dcd9e655ba0473129c3408a4189c806f616c378e6fd18fb3b7',
|
||||||
'com.android.support:support-core-ui:bbc7f65fc95649464733af373361532ab5f9f3b749c3badaa2bbf27e574b6c6f',
|
'com.android.support:support-core-ui:bbc7f65fc95649464733af373361532ab5f9f3b749c3badaa2bbf27e574b6c6f',
|
||||||
'android.arch.persistence.room:runtime:d05c78d494dc700fd6dbc0e873451aebb2510ffbb070c82179055cb10bdd8822',
|
|
||||||
'com.android.support:support-core-utils:c81e1e98ca3cb2edae002c69cf35b22aec364b8cb2f1042c97e206eb5790ac41',
|
'com.android.support:support-core-utils:c81e1e98ca3cb2edae002c69cf35b22aec364b8cb2f1042c97e206eb5790ac41',
|
||||||
'com.android.support:support-vector-drawable:f658986d968172bccfed28578471c96050780fe5e133861e4d331069cc373f4d',
|
'com.android.support:support-vector-drawable:f658986d968172bccfed28578471c96050780fe5e133861e4d331069cc373f4d',
|
||||||
'com.android.support:transition:45d09fc51284c17bbab300f5122512ac7d7348a6d23bda2051648bbe76cc9aa5',
|
'com.android.support:transition:45d09fc51284c17bbab300f5122512ac7d7348a6d23bda2051648bbe76cc9aa5',
|
||||||
|
@ -252,12 +249,8 @@ dependencyVerification {
|
||||||
'com.android.support:localbroadcastmanager:d287c823af5fdde72c099fcfc5f630efe9687af7a914343ae6fd92de32c8a806',
|
'com.android.support:localbroadcastmanager:d287c823af5fdde72c099fcfc5f630efe9687af7a914343ae6fd92de32c8a806',
|
||||||
'com.android.support:print:4be8a812d73e4a80e35b91ceae127def3f0bb9726bf3bc439aa0cc81503f5728',
|
'com.android.support:print:4be8a812d73e4a80e35b91ceae127def3f0bb9726bf3bc439aa0cc81503f5728',
|
||||||
'com.android.support:interpolator:7bc7ee86a0db39a4b51956f3e89842d2bd962118d57d779eb6ed6b34ba0677ea',
|
'com.android.support:interpolator:7bc7ee86a0db39a4b51956f3e89842d2bd962118d57d779eb6ed6b34ba0677ea',
|
||||||
'android.arch.persistence.room:common:fa506873be8a7de9685389b6539ad5849b39731328454b6db151bcab8a9577c3',
|
|
||||||
'android.arch.persistence:db-framework:f9d1629574008e815a494390857f2125cb3e2cfc291aef8b63625bb3fdc5f360',
|
|
||||||
'android.arch.persistence:db:4ed3c473a2da0944203a66a9e84f4c2fb3bca9854c5d4a263a56b1aec4a52e74',
|
|
||||||
'com.android.support:support-annotations:5d5b9414f02d3fa0ee7526b8d5ddae0da67c8ecc8c4d63ffa6cf91488a93b927',
|
'com.android.support:support-annotations:5d5b9414f02d3fa0ee7526b8d5ddae0da67c8ecc8c4d63ffa6cf91488a93b927',
|
||||||
'com.android.support.constraint:constraint-layout-solver:2cafbe356f71c208013d021f32943904798cd6459e5107f9fe27000eb5bc2aef',
|
'com.android.support.constraint:constraint-layout-solver:2cafbe356f71c208013d021f32943904798cd6459e5107f9fe27000eb5bc2aef',
|
||||||
'com.google.guava:listenablefuture:e4ad7607e5c0477c6f890ef26a49cb8d1bb4dffb650bab4502afee64644e3069',
|
|
||||||
'org.signal:signal-metadata-android:d9d798aab7ee7200373ecff8718baf8aaeb632c123604e8a41b7b4c0c97eeee1',
|
'org.signal:signal-metadata-android:d9d798aab7ee7200373ecff8718baf8aaeb632c123604e8a41b7b4c0c97eeee1',
|
||||||
'org.whispersystems:signal-service-java:fde1a008fe42ebbf1cd35018b363135cd8fec9e690304f8917b5ffb7080fa2a5',
|
'org.whispersystems:signal-service-java:fde1a008fe42ebbf1cd35018b363135cd8fec9e690304f8917b5ffb7080fa2a5',
|
||||||
'com.github.bumptech.glide:disklrucache:c1b1b6f5bbd01e2fcdc9d7f60913c8d338bdb65ed4a93bfa02b56f19daaade4b',
|
'com.github.bumptech.glide:disklrucache:c1b1b6f5bbd01e2fcdc9d7f60913c8d338bdb65ed4a93bfa02b56f19daaade4b',
|
||||||
|
@ -373,7 +366,6 @@ android {
|
||||||
'proguard-retrolambda.pro',
|
'proguard-retrolambda.pro',
|
||||||
'proguard-okhttp.pro',
|
'proguard-okhttp.pro',
|
||||||
'proguard-ez-vcard.pro',
|
'proguard-ez-vcard.pro',
|
||||||
'proguard-workmanager.pro',
|
|
||||||
'proguard.cfg'
|
'proguard.cfg'
|
||||||
testProguardFiles 'proguard-automation.pro',
|
testProguardFiles 'proguard-automation.pro',
|
||||||
'proguard.cfg'
|
'proguard.cfg'
|
||||||
|
@ -422,6 +414,7 @@ android {
|
||||||
}
|
}
|
||||||
test {
|
test {
|
||||||
java.srcDirs = ['test/unitTest/java']
|
java.srcDirs = ['test/unitTest/java']
|
||||||
|
resources.srcDirs = ['test/unitTest/resources']
|
||||||
}
|
}
|
||||||
|
|
||||||
website.manifest.srcFile 'website/AndroidManifest.xml'
|
website.manifest.srcFile 'website/AndroidManifest.xml'
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
-dontwarn sun.misc.Unsafe
|
|
||||||
-dontwarn com.google.common.util.concurrent.ListenableFuture
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<bool name="enable_alarm_manager">false</bool>
|
||||||
|
<bool name="enable_job_service">true</bool>
|
||||||
|
</resources>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<bool name="enable_alarm_manager">true</bool>
|
||||||
|
<bool name="enable_job_service">false</bool>
|
||||||
|
</resources>
|
|
@ -32,12 +32,14 @@ import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||||
import org.thoughtcrime.securesms.crypto.PRNGFixes;
|
import org.thoughtcrime.securesms.crypto.PRNGFixes;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
|
||||||
import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule;
|
import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule;
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||||
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule;
|
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.DependencyInjector;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
import org.thoughtcrime.securesms.jobmanager.dependencies.DependencyInjector;
|
import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
||||||
|
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||||
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
||||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||||
|
@ -71,8 +73,6 @@ import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import androidx.work.Configuration;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
import dagger.ObjectGraph;
|
import dagger.ObjectGraph;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -90,7 +90,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||||
private ExpiringMessageManager expiringMessageManager;
|
private ExpiringMessageManager expiringMessageManager;
|
||||||
private TypingStatusRepository typingStatusRepository;
|
private TypingStatusRepository typingStatusRepository;
|
||||||
private TypingStatusSender typingStatusSender;
|
private TypingStatusSender typingStatusSender;
|
||||||
private JobManager jobManager;
|
private JobManager jobManager;
|
||||||
private IncomingMessageObserver incomingMessageObserver;
|
private IncomingMessageObserver incomingMessageObserver;
|
||||||
private ObjectGraph objectGraph;
|
private ObjectGraph objectGraph;
|
||||||
private PersistentLogger persistentLogger;
|
private PersistentLogger persistentLogger;
|
||||||
|
@ -189,11 +189,14 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeJobManager() {
|
private void initializeJobManager() {
|
||||||
WorkManager.initialize(this, new Configuration.Builder()
|
this.jobManager = new JobManager(this, new JobManager.Configuration.Builder()
|
||||||
.setMinimumLoggingLevel(android.util.Log.INFO)
|
.setDataSerializer(new JsonDataSerializer())
|
||||||
.build());
|
.setJobFactories(JobManagerFactories.getJobFactories(this))
|
||||||
|
.setConstraintFactories(JobManagerFactories.getConstraintFactories(this))
|
||||||
this.jobManager = new JobManager(this, WorkManager.getInstance());
|
.setConstraintObservers(JobManagerFactories.getConstraintObservers(this))
|
||||||
|
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(this)))
|
||||||
|
.setDependencyInjector(this)
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initializeMessageRetrieval() {
|
public void initializeMessageRetrieval() {
|
||||||
|
@ -210,7 +213,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||||
long nextSetTime = TextSecurePreferences.getFcmTokenLastSetTime(this) + TimeUnit.HOURS.toMillis(6);
|
long nextSetTime = TextSecurePreferences.getFcmTokenLastSetTime(this) + TimeUnit.HOURS.toMillis(6);
|
||||||
|
|
||||||
if (TextSecurePreferences.getFcmToken(this) == null || nextSetTime <= System.currentTimeMillis()) {
|
if (TextSecurePreferences.getFcmToken(this) == null || nextSetTime <= System.currentTimeMillis()) {
|
||||||
this.jobManager.add(new FcmRefreshJob(this));
|
this.jobManager.add(new FcmRefreshJob());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -312,7 +315,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||||
|
|
||||||
private void initializeUnidentifiedDeliveryAbilityRefresh() {
|
private void initializeUnidentifiedDeliveryAbilityRefresh() {
|
||||||
if (TextSecurePreferences.isMultiDevice(this) && !TextSecurePreferences.isUnidentifiedDeliveryEnabled(this)) {
|
if (TextSecurePreferences.isMultiDevice(this) && !TextSecurePreferences.isUnidentifiedDeliveryEnabled(this)) {
|
||||||
jobManager.add(new RefreshUnidentifiedDeliveryAbilityJob(this));
|
jobManager.add(new RefreshUnidentifiedDeliveryAbilityJob());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,8 +27,6 @@ import android.graphics.BitmapFactory;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -202,7 +200,7 @@ public class ConversationListFragment extends Fragment
|
||||||
} else if (ExpiredBuildReminder.isEligible()) {
|
} else if (ExpiredBuildReminder.isEligible()) {
|
||||||
return Optional.of(new ExpiredBuildReminder(context));
|
return Optional.of(new ExpiredBuildReminder(context));
|
||||||
} else if (ServiceOutageReminder.isEligible(context)) {
|
} else if (ServiceOutageReminder.isEligible(context)) {
|
||||||
ApplicationContext.getInstance(context).getJobManager().add(new ServiceOutageDetectionJob(context));
|
ApplicationContext.getInstance(context).getJobManager().add(new ServiceOutageDetectionJob());
|
||||||
return Optional.of(new ServiceOutageReminder(context));
|
return Optional.of(new ServiceOutageReminder(context));
|
||||||
} else if (OutdatedBuildReminder.isEligible()) {
|
} else if (OutdatedBuildReminder.isEligible()) {
|
||||||
return Optional.of(new OutdatedBuildReminder(context));
|
return Optional.of(new OutdatedBuildReminder(context));
|
||||||
|
|
|
@ -381,7 +381,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceProfileKeyUpdateJob(context));
|
ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceProfileKeyUpdateJob());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,10 +26,6 @@ import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v7.preference.PreferenceManager;
|
import android.support.v7.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.persistence.JavaJobSerializer;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.persistence.PersistentStorage;
|
|
||||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy;
|
import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
@ -261,7 +257,7 @@ public class DatabaseUpgradeActivity extends BaseActivity {
|
||||||
if (params[0] < CONTACTS_ACCOUNT_VERSION) {
|
if (params[0] < CONTACTS_ACCOUNT_VERSION) {
|
||||||
ApplicationContext.getInstance(getApplicationContext())
|
ApplicationContext.getInstance(getApplicationContext())
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new DirectoryRefreshJob(getApplicationContext(), false));
|
.add(new DirectoryRefreshJob(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params[0] < MEDIA_DOWNLOAD_CONTROLS_VERSION) {
|
if (params[0] < MEDIA_DOWNLOAD_CONTROLS_VERSION) {
|
||||||
|
@ -271,16 +267,16 @@ public class DatabaseUpgradeActivity extends BaseActivity {
|
||||||
if (params[0] < REDPHONE_SUPPORT_VERSION) {
|
if (params[0] < REDPHONE_SUPPORT_VERSION) {
|
||||||
ApplicationContext.getInstance(getApplicationContext())
|
ApplicationContext.getInstance(getApplicationContext())
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new RefreshAttributesJob(getApplicationContext()));
|
.add(new RefreshAttributesJob());
|
||||||
ApplicationContext.getInstance(getApplicationContext())
|
ApplicationContext.getInstance(getApplicationContext())
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new DirectoryRefreshJob(getApplicationContext(), false));
|
.add(new DirectoryRefreshJob(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params[0] < PROFILES) {
|
if (params[0] < PROFILES) {
|
||||||
ApplicationContext.getInstance(getApplicationContext())
|
ApplicationContext.getInstance(getApplicationContext())
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new DirectoryRefreshJob(getApplicationContext(), false));
|
.add(new DirectoryRefreshJob(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params[0] < SCREENSHOTS) {
|
if (params[0] < SCREENSHOTS) {
|
||||||
|
@ -335,17 +331,18 @@ public class DatabaseUpgradeActivity extends BaseActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params[0] < WORKMANAGER_MIGRATION) {
|
// This migration became unnecessary after switching away from WorkManager
|
||||||
Log.i(TAG, "Beginning migration of existing jobs to WorkManager");
|
// if (params[0] < WORKMANAGER_MIGRATION) {
|
||||||
|
// Log.i(TAG, "Beginning migration of existing jobs to WorkManager");
|
||||||
JobManager jobManager = ApplicationContext.getInstance(getApplicationContext()).getJobManager();
|
//
|
||||||
PersistentStorage storage = new PersistentStorage(getApplicationContext(), "TextSecureJobs", new JavaJobSerializer());
|
// JobManager jobManager = ApplicationContext.getInstance(getApplicationContext()).getJobManager();
|
||||||
|
// PersistentStorage storage = new PersistentStorage(getApplicationContext(), "TextSecureJobs", new JavaJobSerializer());
|
||||||
for (Job job : storage.getAllUnencrypted()) {
|
//
|
||||||
jobManager.add(job);
|
// for (Job job : storage.getAllUnencrypted()) {
|
||||||
Log.i(TAG, "Migrated job with class '" + job.getClass().getSimpleName() + "' to run on new JobManager.");
|
// jobManager.add(job);
|
||||||
}
|
// Log.i(TAG, "Migrated job with class '" + job.getClass().getSimpleName() + "' to run on new JobManager.");
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
if (params[0] < COLOR_MIGRATION) {
|
if (params[0] < COLOR_MIGRATION) {
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
@ -372,14 +369,14 @@ public class DatabaseUpgradeActivity extends BaseActivity {
|
||||||
Log.i(TAG, "Scheduling UD attributes refresh.");
|
Log.i(TAG, "Scheduling UD attributes refresh.");
|
||||||
ApplicationContext.getInstance(context)
|
ApplicationContext.getInstance(context)
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new RefreshAttributesJob(context));
|
.add(new RefreshAttributesJob());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params[0] < SIGNALING_KEY_DEPRECATION) {
|
if (params[0] < SIGNALING_KEY_DEPRECATION) {
|
||||||
Log.i(TAG, "Scheduling a RefreshAttributesJob to remove the signaling key remotely.");
|
Log.i(TAG, "Scheduling a RefreshAttributesJob to remove the signaling key remotely.");
|
||||||
ApplicationContext.getInstance(context)
|
ApplicationContext.getInstance(context)
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new RefreshAttributesJob(context));
|
.add(new RefreshAttributesJob());
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -402,7 +399,7 @@ public class DatabaseUpgradeActivity extends BaseActivity {
|
||||||
Log.i(TAG, "queuing new attachment download job for incoming push part " + attachment.getAttachmentId() + ".");
|
Log.i(TAG, "queuing new attachment download job for incoming push part " + attachment.getAttachmentId() + ".");
|
||||||
ApplicationContext.getInstance(context)
|
ApplicationContext.getInstance(context)
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new AttachmentDownloadJob(context, attachment.getMmsId(), attachment.getAttachmentId(), false));
|
.add(new AttachmentDownloadJob(attachment.getMmsId(), attachment.getAttachmentId(), false));
|
||||||
}
|
}
|
||||||
reader.close();
|
reader.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,7 +177,7 @@ public class DeviceListFragment extends ListFragment
|
||||||
|
|
||||||
ApplicationContext.getInstance(getContext())
|
ApplicationContext.getInstance(getContext())
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new RefreshUnidentifiedDeliveryAbilityJob(getContext()));
|
.add(new RefreshUnidentifiedDeliveryAbilityJob());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();
|
Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();
|
||||||
|
|
|
@ -8,7 +8,6 @@ import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
|
||||||
|
@ -48,8 +47,7 @@ public class LinkPreviewsIntroFragment extends Fragment {
|
||||||
view.findViewById(R.id.experience_ok_button).setOnClickListener(v -> {
|
view.findViewById(R.id.experience_ok_button).setOnClickListener(v -> {
|
||||||
ApplicationContext.getInstance(requireContext())
|
ApplicationContext.getInstance(requireContext())
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
|
.add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
||||||
TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
|
||||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
||||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(requireContext()),
|
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(requireContext()),
|
||||||
TextSecurePreferences.isLinkPreviewsEnabled(requireContext())));
|
TextSecurePreferences.isLinkPreviewsEnabled(requireContext())));
|
||||||
|
|
|
@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||||
import org.thoughtcrime.securesms.registration.WelcomeActivity;
|
import org.thoughtcrime.securesms.registration.WelcomeActivity;
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
|
|
@ -38,8 +38,7 @@ public class ReadReceiptsIntroFragment extends Fragment {
|
||||||
TextSecurePreferences.setReadReceiptsEnabled(getContext(), isChecked);
|
TextSecurePreferences.setReadReceiptsEnabled(getContext(), isChecked);
|
||||||
ApplicationContext.getInstance(getContext())
|
ApplicationContext.getInstance(getContext())
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
|
.add(new MultiDeviceConfigurationUpdateJob(isChecked,
|
||||||
isChecked,
|
|
||||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
||||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
||||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||||
|
|
|
@ -771,12 +771,12 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
||||||
if (blocked && (recipient.resolve().isSystemContact() || recipient.resolve().isProfileSharing())) {
|
if (blocked && (recipient.resolve().isSystemContact() || recipient.resolve().isProfileSharing())) {
|
||||||
ApplicationContext.getInstance(context)
|
ApplicationContext.getInstance(context)
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new RotateProfileKeyJob(context));
|
.add(new RotateProfileKeyJob());
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplicationContext.getInstance(context)
|
ApplicationContext.getInstance(context)
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new MultiDeviceBlockedUpdateJob(context));
|
.add(new MultiDeviceBlockedUpdateJob());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
|
|
|
@ -91,7 +91,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
|
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
|
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
|
||||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
|
||||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
||||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||||
|
|
||||||
|
@ -729,7 +728,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSuccessfulRegistration() {
|
private void handleSuccessfulRegistration() {
|
||||||
ApplicationContext.getInstance(RegistrationActivity.this).getJobManager().add(new DirectoryRefreshJob(RegistrationActivity.this, false));
|
ApplicationContext.getInstance(RegistrationActivity.this).getJobManager().add(new DirectoryRefreshJob(false));
|
||||||
ApplicationContext.getInstance(RegistrationActivity.this).getJobManager().add(new RotateCertificateJob(RegistrationActivity.this));
|
ApplicationContext.getInstance(RegistrationActivity.this).getJobManager().add(new RotateCertificateJob(RegistrationActivity.this));
|
||||||
|
|
||||||
DirectoryRefreshListener.schedule(RegistrationActivity.this);
|
DirectoryRefreshListener.schedule(RegistrationActivity.this);
|
||||||
|
|
|
@ -59,8 +59,7 @@ public class TypingIndicatorIntroFragment extends Fragment {
|
||||||
TextSecurePreferences.setTypingIndicatorsEnabled(getContext(), typingEnabled);
|
TextSecurePreferences.setTypingIndicatorsEnabled(getContext(), typingEnabled);
|
||||||
ApplicationContext.getInstance(requireContext())
|
ApplicationContext.getInstance(requireContext())
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
|
.add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
||||||
TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
|
||||||
typingEnabled,
|
typingEnabled,
|
||||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
||||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||||
|
|
|
@ -603,8 +603,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
|
||||||
|
|
||||||
ApplicationContext.getInstance(getActivity())
|
ApplicationContext.getInstance(getActivity())
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new MultiDeviceVerifiedUpdateJob(getActivity(),
|
.add(new MultiDeviceVerifiedUpdateJob(recipient.getAddress(),
|
||||||
recipient.getAddress(),
|
|
||||||
remoteIdentity,
|
remoteIdentity,
|
||||||
isChecked ? VerifiedStatus.VERIFIED :
|
isChecked ? VerifiedStatus.VERIFIED :
|
||||||
VerifiedStatus.DEFAULT));
|
VerifiedStatus.DEFAULT));
|
||||||
|
|
|
@ -6,8 +6,6 @@ import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -76,7 +74,7 @@ public class TypingStatusSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendTyping(long threadId, boolean typingStarted) {
|
private void sendTyping(long threadId, boolean typingStarted) {
|
||||||
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(context, threadId, typingStarted));
|
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(threadId, typingStarted));
|
||||||
}
|
}
|
||||||
|
|
||||||
private class StartRunnable implements Runnable {
|
private class StartRunnable implements Runnable {
|
||||||
|
|
|
@ -36,10 +36,7 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||||
import android.view.OrientationEventListener;
|
import android.view.OrientationEventListener;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
|
@ -253,7 +253,7 @@ public class SharedContactDetailsActivity extends PassphraseRequiredActionBarAct
|
||||||
if (requestCode == CODE_ADD_EDIT_CONTACT && contact != null) {
|
if (requestCode == CODE_ADD_EDIT_CONTACT && contact != null) {
|
||||||
ApplicationContext.getInstance(getApplicationContext())
|
ApplicationContext.getInstance(getApplicationContext())
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new DirectoryRefreshJob(getApplicationContext(), false));
|
.add(new DirectoryRefreshJob(false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -852,7 +852,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||||
|
|
||||||
ApplicationContext.getInstance(ConversationActivity.this)
|
ApplicationContext.getInstance(ConversationActivity.this)
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new MultiDeviceBlockedUpdateJob(ConversationActivity.this));
|
.add(new MultiDeviceBlockedUpdateJob());
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1377,7 +1377,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||||
} else if (ExpiredBuildReminder.isEligible()) {
|
} else if (ExpiredBuildReminder.isEligible()) {
|
||||||
reminderView.get().showReminder(new ExpiredBuildReminder(this));
|
reminderView.get().showReminder(new ExpiredBuildReminder(this));
|
||||||
} else if (ServiceOutageReminder.isEligible(this)) {
|
} else if (ServiceOutageReminder.isEligible(this)) {
|
||||||
ApplicationContext.getInstance(this).getJobManager().add(new ServiceOutageDetectionJob(this));
|
ApplicationContext.getInstance(this).getJobManager().add(new ServiceOutageDetectionJob());
|
||||||
reminderView.get().showReminder(new ServiceOutageReminder(this));
|
reminderView.get().showReminder(new ServiceOutageReminder(this));
|
||||||
} else if (TextSecurePreferences.isPushRegistered(this) &&
|
} else if (TextSecurePreferences.isPushRegistered(this) &&
|
||||||
TextSecurePreferences.isShowInviteReminders(this) &&
|
TextSecurePreferences.isShowInviteReminders(this) &&
|
||||||
|
@ -1629,7 +1629,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||||
|
|
||||||
ApplicationContext.getInstance(this)
|
ApplicationContext.getInstance(this)
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new RetrieveProfileJob(this, recipient));
|
.add(new RetrieveProfileJob(recipient));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -992,7 +992,7 @@ public class ConversationFragment extends Fragment
|
||||||
if (requestCode == CODE_ADD_EDIT_CONTACT && getContext() != null) {
|
if (requestCode == CODE_ADD_EDIT_CONTACT && getContext() != null) {
|
||||||
ApplicationContext.getInstance(getContext().getApplicationContext())
|
ApplicationContext.getInstance(getContext().getApplicationContext())
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new DirectoryRefreshJob(getContext().getApplicationContext(), false));
|
.add(new DirectoryRefreshJob(false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1043,7 +1043,7 @@ public class ConversationItem extends LinearLayout
|
||||||
Log.i(TAG, "Scheduling MMS attachment download");
|
Log.i(TAG, "Scheduling MMS attachment download");
|
||||||
ApplicationContext.getInstance(context)
|
ApplicationContext.getInstance(context)
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new MmsDownloadJob(context, messageRecord.getId(),
|
.add(new MmsDownloadJob(messageRecord.getId(),
|
||||||
messageRecord.getThreadId(), false));
|
messageRecord.getThreadId(), false));
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "Scheduling push attachment downloads for " + slides.size() + " items");
|
Log.i(TAG, "Scheduling push attachment downloads for " + slides.size() + " items");
|
||||||
|
@ -1051,7 +1051,7 @@ public class ConversationItem extends LinearLayout
|
||||||
for (Slide slide : slides) {
|
for (Slide slide : slides) {
|
||||||
ApplicationContext.getInstance(context)
|
ApplicationContext.getInstance(context)
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new AttachmentDownloadJob(context, messageRecord.getId(),
|
.add(new AttachmentDownloadJob(messageRecord.getId(),
|
||||||
((DatabaseAttachment)slide.asAttachment()).getAttachmentId(), true));
|
((DatabaseAttachment)slide.asAttachment()).getAttachmentId(), true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1171,7 +1171,7 @@ public class ConversationItem extends LinearLayout
|
||||||
|
|
||||||
ApplicationContext.getInstance(context)
|
ApplicationContext.getInstance(context)
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new MmsSendJob(context, messageRecord.getId()));
|
.add(new MmsSendJob(messageRecord.getId()));
|
||||||
} else {
|
} else {
|
||||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||||
database.markAsInsecure(messageRecord.getId());
|
database.markAsInsecure(messageRecord.getId());
|
||||||
|
|
|
@ -57,6 +57,7 @@ public class DatabaseFactory {
|
||||||
private final SignedPreKeyDatabase signedPreKeyDatabase;
|
private final SignedPreKeyDatabase signedPreKeyDatabase;
|
||||||
private final SessionDatabase sessionDatabase;
|
private final SessionDatabase sessionDatabase;
|
||||||
private final SearchDatabase searchDatabase;
|
private final SearchDatabase searchDatabase;
|
||||||
|
private final JobDatabase jobDatabase;
|
||||||
|
|
||||||
public static DatabaseFactory getInstance(Context context) {
|
public static DatabaseFactory getInstance(Context context) {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
|
@ -135,6 +136,10 @@ public class DatabaseFactory {
|
||||||
return getInstance(context).searchDatabase;
|
return getInstance(context).searchDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static JobDatabase getJobDatabase(Context context) {
|
||||||
|
return getInstance(context).jobDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
public static SQLiteDatabase getBackupDatabase(Context context) {
|
public static SQLiteDatabase getBackupDatabase(Context context) {
|
||||||
return getInstance(context).databaseHelper.getReadableDatabase();
|
return getInstance(context).databaseHelper.getReadableDatabase();
|
||||||
}
|
}
|
||||||
|
@ -168,6 +173,7 @@ public class DatabaseFactory {
|
||||||
this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper);
|
this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper);
|
||||||
this.sessionDatabase = new SessionDatabase(context, databaseHelper);
|
this.sessionDatabase = new SessionDatabase(context, databaseHelper);
|
||||||
this.searchDatabase = new SearchDatabase(context, databaseHelper);
|
this.searchDatabase = new SearchDatabase(context, databaseHelper);
|
||||||
|
this.jobDatabase = new JobDatabase(context, databaseHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,
|
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,
|
||||||
|
|
|
@ -0,0 +1,242 @@
|
||||||
|
package org.thoughtcrime.securesms.database;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
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 java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class JobDatabase extends Database {
|
||||||
|
|
||||||
|
public static final String[] CREATE_TABLE = new String[] { Jobs.CREATE_TABLE,
|
||||||
|
Constraints.CREATE_TABLE,
|
||||||
|
Dependencies.CREATE_TABLE };
|
||||||
|
|
||||||
|
private static final class Jobs {
|
||||||
|
private static final String TABLE_NAME = "job_spec";
|
||||||
|
private static final String ID = "_id";
|
||||||
|
private static final String JOB_SPEC_ID = "job_spec_id";
|
||||||
|
private static final String FACTORY_KEY = "factory_key";
|
||||||
|
private static final String QUEUE_KEY = "queue_key";
|
||||||
|
private static final String CREATE_TIME = "create_time";
|
||||||
|
private static final String NEXT_RUN_ATTEMPT_TIME = "next_run_attempt_time";
|
||||||
|
private static final String RUN_ATTEMPT = "run_attempt";
|
||||||
|
private static final String MAX_ATTEMPTS = "max_attempts";
|
||||||
|
private static final String MAX_BACKOFF = "max_backoff";
|
||||||
|
private static final String MAX_INSTANCES = "max_instances";
|
||||||
|
private static final String LIFESPAN = "lifespan";
|
||||||
|
private static final String SERIALIZED_DATA = "serialized_data";
|
||||||
|
private static final String IS_RUNNING = "is_running";
|
||||||
|
|
||||||
|
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||||
|
JOB_SPEC_ID + " TEXT UNIQUE, " +
|
||||||
|
FACTORY_KEY + " TEXT, " +
|
||||||
|
QUEUE_KEY + " TEXT, " +
|
||||||
|
CREATE_TIME + " INTEGER, " +
|
||||||
|
NEXT_RUN_ATTEMPT_TIME + " INTEGER, " +
|
||||||
|
RUN_ATTEMPT + " INTEGER, " +
|
||||||
|
MAX_ATTEMPTS + " INTEGER, " +
|
||||||
|
MAX_BACKOFF + " INTEGER, " +
|
||||||
|
MAX_INSTANCES + " INTEGER, " +
|
||||||
|
LIFESPAN + " INTEGER, " +
|
||||||
|
SERIALIZED_DATA + " TEXT, " +
|
||||||
|
IS_RUNNING + " INTEGER)";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Constraints {
|
||||||
|
private static final String TABLE_NAME = "constraint_spec";
|
||||||
|
private static final String ID = "_id";
|
||||||
|
private static final String JOB_SPEC_ID = "job_spec_id";
|
||||||
|
private static final String FACTORY_KEY = "factory_key";
|
||||||
|
|
||||||
|
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||||
|
JOB_SPEC_ID + " TEXT, " +
|
||||||
|
FACTORY_KEY + " TEXT, " +
|
||||||
|
"UNIQUE(" + JOB_SPEC_ID + ", " + FACTORY_KEY + "))";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Dependencies {
|
||||||
|
private static final String TABLE_NAME = "dependency_spec";
|
||||||
|
private static final String ID = "_id";
|
||||||
|
private static final String JOB_SPEC_ID = "job_spec_id";
|
||||||
|
private static final String DEPENDS_ON_JOB_SPEC_ID = "depends_on_job_spec_id";
|
||||||
|
|
||||||
|
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||||
|
JOB_SPEC_ID + " TEXT, " +
|
||||||
|
DEPENDS_ON_JOB_SPEC_ID + " TEXT, " +
|
||||||
|
"UNIQUE(" + JOB_SPEC_ID + ", " + DEPENDS_ON_JOB_SPEC_ID + "))";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public JobDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||||
|
super(context, databaseHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void insertJobs(@NonNull List<FullSpec> fullSpecs) {
|
||||||
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
|
||||||
|
db.beginTransaction();
|
||||||
|
|
||||||
|
for (FullSpec fullSpec : fullSpecs) {
|
||||||
|
insertJobSpec(db, fullSpec.getJobSpec());
|
||||||
|
insertConstraintSpecs(db, fullSpec.getConstraintSpecs());
|
||||||
|
insertDependencySpecs(db, fullSpec.getDependencySpecs());
|
||||||
|
}
|
||||||
|
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
db.endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized @NonNull List<JobSpec> getAllJobSpecs() {
|
||||||
|
List<JobSpec> jobs = new LinkedList<>();
|
||||||
|
|
||||||
|
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Jobs.TABLE_NAME, null, null, null, null, null, Jobs.CREATE_TIME + ", " + Jobs.ID + " ASC")) {
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
jobs.add(jobSpecFromCursor(cursor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void updateJobRunningState(@NonNull String id, boolean isRunning) {
|
||||||
|
ContentValues contentValues = new ContentValues();
|
||||||
|
contentValues.put(Jobs.IS_RUNNING, isRunning ? 1 : 0);
|
||||||
|
|
||||||
|
String query = Jobs.JOB_SPEC_ID + " = ?";
|
||||||
|
String[] args = new String[]{ id };
|
||||||
|
|
||||||
|
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime) {
|
||||||
|
ContentValues contentValues = new ContentValues();
|
||||||
|
contentValues.put(Jobs.IS_RUNNING, isRunning ? 1 : 0);
|
||||||
|
contentValues.put(Jobs.RUN_ATTEMPT, runAttempt);
|
||||||
|
contentValues.put(Jobs.NEXT_RUN_ATTEMPT_TIME, nextRunAttemptTime);
|
||||||
|
|
||||||
|
String query = Jobs.JOB_SPEC_ID + " = ?";
|
||||||
|
String[] args = new String[]{ id };
|
||||||
|
|
||||||
|
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void updateAllJobsToBePending() {
|
||||||
|
ContentValues contentValues = new ContentValues();
|
||||||
|
contentValues.put(Jobs.IS_RUNNING, 0);
|
||||||
|
|
||||||
|
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void deleteJobs(@NonNull List<String> jobIds) {
|
||||||
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
|
||||||
|
db.beginTransaction();
|
||||||
|
for (String jobId : jobIds) {
|
||||||
|
String[] arg = new String[]{jobId};
|
||||||
|
|
||||||
|
db.delete(Jobs.TABLE_NAME, Jobs.JOB_SPEC_ID + " = ?", arg);
|
||||||
|
db.delete(Constraints.TABLE_NAME, Constraints.JOB_SPEC_ID + " = ?", arg);
|
||||||
|
db.delete(Dependencies.TABLE_NAME, Dependencies.JOB_SPEC_ID + " = ?", arg);
|
||||||
|
db.delete(Dependencies.TABLE_NAME, Dependencies.DEPENDS_ON_JOB_SPEC_ID + " = ?", arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
db.endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized @NonNull List<ConstraintSpec> getAllConstraintSpecs() {
|
||||||
|
List<ConstraintSpec> constraints = new LinkedList<>();
|
||||||
|
|
||||||
|
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Constraints.TABLE_NAME, null, null, null, null, null, null)) {
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
constraints.add(constraintSpecFromCursor(cursor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return constraints;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized @NonNull List<DependencySpec> getAllDependencySpecs() {
|
||||||
|
List<DependencySpec> dependencies = new LinkedList<>();
|
||||||
|
|
||||||
|
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Dependencies.TABLE_NAME, null, null, null, null, null, null)) {
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
dependencies.add(dependencySpecFromCursor(cursor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertJobSpec(@NonNull SQLiteDatabase db, @NonNull JobSpec job) {
|
||||||
|
ContentValues contentValues = new ContentValues();
|
||||||
|
contentValues.put(Jobs.JOB_SPEC_ID, job.getId());
|
||||||
|
contentValues.put(Jobs.FACTORY_KEY, job.getFactoryKey());
|
||||||
|
contentValues.put(Jobs.QUEUE_KEY, job.getQueueKey());
|
||||||
|
contentValues.put(Jobs.CREATE_TIME, job.getCreateTime());
|
||||||
|
contentValues.put(Jobs.NEXT_RUN_ATTEMPT_TIME, job.getNextRunAttemptTime());
|
||||||
|
contentValues.put(Jobs.RUN_ATTEMPT, job.getRunAttempt());
|
||||||
|
contentValues.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts());
|
||||||
|
contentValues.put(Jobs.MAX_BACKOFF, job.getMaxBackoff());
|
||||||
|
contentValues.put(Jobs.MAX_INSTANCES, job.getMaxInstances());
|
||||||
|
contentValues.put(Jobs.LIFESPAN, job.getLifespan());
|
||||||
|
contentValues.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
|
||||||
|
contentValues.put(Jobs.IS_RUNNING, job.isRunning() ? 1 : 0);
|
||||||
|
|
||||||
|
db.insertWithOnConflict(Jobs.TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertConstraintSpecs(@NonNull SQLiteDatabase db, @NonNull List<ConstraintSpec> constraints) {
|
||||||
|
for (ConstraintSpec constraintSpec : constraints) {
|
||||||
|
ContentValues contentValues = new ContentValues();
|
||||||
|
contentValues.put(Constraints.JOB_SPEC_ID, constraintSpec.getJobSpecId());
|
||||||
|
contentValues.put(Constraints.FACTORY_KEY, constraintSpec.getFactoryKey());
|
||||||
|
db.insertWithOnConflict(Constraints.TABLE_NAME, null ,contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertDependencySpecs(@NonNull SQLiteDatabase db, @NonNull List<DependencySpec> dependencies) {
|
||||||
|
for (DependencySpec dependencySpec : dependencies) {
|
||||||
|
ContentValues contentValues = new ContentValues();
|
||||||
|
contentValues.put(Dependencies.JOB_SPEC_ID, dependencySpec.getJobId());
|
||||||
|
contentValues.put(Dependencies.DEPENDS_ON_JOB_SPEC_ID, dependencySpec.getDependsOnJobId());
|
||||||
|
db.insertWithOnConflict(Dependencies.TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull JobSpec jobSpecFromCursor(@NonNull Cursor cursor) {
|
||||||
|
return new JobSpec(cursor.getString(cursor.getColumnIndexOrThrow(Jobs.JOB_SPEC_ID)),
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.FACTORY_KEY)),
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.QUEUE_KEY)),
|
||||||
|
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.CREATE_TIME)),
|
||||||
|
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.NEXT_RUN_ATTEMPT_TIME)),
|
||||||
|
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.RUN_ATTEMPT)),
|
||||||
|
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_ATTEMPTS)),
|
||||||
|
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.MAX_BACKOFF)),
|
||||||
|
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.LIFESPAN)),
|
||||||
|
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_INSTANCES)),
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_DATA)),
|
||||||
|
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.IS_RUNNING)) == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull ConstraintSpec constraintSpecFromCursor(@NonNull Cursor cursor) {
|
||||||
|
return new ConstraintSpec(cursor.getString(cursor.getColumnIndexOrThrow(Constraints.JOB_SPEC_ID)),
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow(Constraints.FACTORY_KEY)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull DependencySpec dependencySpecFromCursor(@NonNull Cursor cursor) {
|
||||||
|
return new DependencySpec(cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.JOB_SPEC_ID)),
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.DEPENDS_ON_JOB_SPEC_ID)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,7 +50,6 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.Quote;
|
import org.thoughtcrime.securesms.database.model.Quote;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
|
||||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
@ -176,11 +175,8 @@ public class MmsDatabase extends MessagingDatabase {
|
||||||
private final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache();
|
private final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache();
|
||||||
private final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache();
|
private final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache();
|
||||||
|
|
||||||
private final JobManager jobManager;
|
|
||||||
|
|
||||||
public MmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
public MmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||||
super(context, databaseHelper);
|
super(context, databaseHelper);
|
||||||
this.jobManager = ApplicationContext.getInstance(context).getJobManager();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -837,7 +833,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyConversationListeners(threadId);
|
notifyConversationListeners(threadId);
|
||||||
jobManager.add(new TrimThreadJob(context, threadId));
|
ApplicationContext.getInstance(context).getJobManager().add(new TrimThreadJob(threadId));
|
||||||
|
|
||||||
return Optional.of(new InsertResult(messageId, threadId));
|
return Optional.of(new InsertResult(messageId, threadId));
|
||||||
}
|
}
|
||||||
|
@ -918,7 +914,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||||
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
|
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
jobManager.add(new TrimThreadJob(context, threadId));
|
ApplicationContext.getInstance(context).getJobManager().add(new TrimThreadJob(threadId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public long insertMessageOutbox(@NonNull OutgoingMediaMessage message,
|
public long insertMessageOutbox(@NonNull OutgoingMediaMessage message,
|
||||||
|
@ -983,7 +979,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||||
|
|
||||||
DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId);
|
DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId);
|
||||||
DatabaseFactory.getThreadDatabase(context).setHasSent(threadId, true);
|
DatabaseFactory.getThreadDatabase(context).setHasSent(threadId, true);
|
||||||
jobManager.add(new TrimThreadJob(context, threadId));
|
ApplicationContext.getInstance(context).getJobManager().add(new TrimThreadJob(threadId));
|
||||||
|
|
||||||
return messageId;
|
return messageId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,6 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
|
||||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
@ -104,11 +103,8 @@ public class SmsDatabase extends MessagingDatabase {
|
||||||
private static final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache();
|
private static final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache();
|
||||||
private static final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache();
|
private static final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache();
|
||||||
|
|
||||||
private final JobManager jobManager;
|
|
||||||
|
|
||||||
public SmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
public SmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||||
super(context, databaseHelper);
|
super(context, databaseHelper);
|
||||||
this.jobManager = ApplicationContext.getInstance(context).getJobManager();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getTableName() {
|
protected String getTableName() {
|
||||||
|
@ -473,7 +469,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||||
DatabaseFactory.getThreadDatabase(context).update(record.getThreadId(), true);
|
DatabaseFactory.getThreadDatabase(context).update(record.getThreadId(), true);
|
||||||
notifyConversationListeners(record.getThreadId());
|
notifyConversationListeners(record.getThreadId());
|
||||||
|
|
||||||
jobManager.add(new TrimThreadJob(context, record.getThreadId()));
|
ApplicationContext.getInstance(context).getJobManager().add(new TrimThreadJob(record.getThreadId()));
|
||||||
|
|
||||||
return new Pair<>(newMessageId, record.getThreadId());
|
return new Pair<>(newMessageId, record.getThreadId());
|
||||||
} catch (NoSuchMessageException e) {
|
} catch (NoSuchMessageException e) {
|
||||||
|
@ -511,7 +507,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||||
|
|
||||||
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
|
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
|
||||||
notifyConversationListeners(threadId);
|
notifyConversationListeners(threadId);
|
||||||
jobManager.add(new TrimThreadJob(context, threadId));
|
ApplicationContext.getInstance(context).getJobManager().add(new TrimThreadJob(threadId));
|
||||||
|
|
||||||
if (unread) {
|
if (unread) {
|
||||||
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
|
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
|
||||||
|
@ -604,7 +600,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||||
notifyConversationListeners(threadId);
|
notifyConversationListeners(threadId);
|
||||||
|
|
||||||
if (!message.isIdentityUpdate() && !message.isIdentityVerified() && !message.isIdentityDefault()) {
|
if (!message.isIdentityUpdate() && !message.isIdentityVerified() && !message.isIdentityDefault()) {
|
||||||
jobManager.add(new TrimThreadJob(context, threadId));
|
ApplicationContext.getInstance(context).getJobManager().add(new TrimThreadJob(threadId));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Optional.of(new InsertResult(messageId, threadId));
|
return Optional.of(new InsertResult(messageId, threadId));
|
||||||
|
@ -662,7 +658,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||||
notifyConversationListeners(threadId);
|
notifyConversationListeners(threadId);
|
||||||
|
|
||||||
if (!message.isIdentityVerified() && !message.isIdentityDefault()) {
|
if (!message.isIdentityVerified() && !message.isIdentityDefault()) {
|
||||||
jobManager.add(new TrimThreadJob(context, threadId));
|
ApplicationContext.getInstance(context).getJobManager().add(new TrimThreadJob(threadId));
|
||||||
}
|
}
|
||||||
|
|
||||||
return messageId;
|
return messageId;
|
||||||
|
|
|
@ -10,6 +10,7 @@ import android.support.annotation.NonNull;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.Address;
|
import org.thoughtcrime.securesms.database.Address;
|
||||||
|
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
@ -63,8 +64,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||||
private static final int CONVERSATION_SEARCH = 17;
|
private static final int CONVERSATION_SEARCH = 17;
|
||||||
private static final int SELF_ATTACHMENT_CLEANUP = 18;
|
private static final int SELF_ATTACHMENT_CLEANUP = 18;
|
||||||
private static final int RECIPIENT_FORCE_SMS_SELECTION = 19;
|
private static final int RECIPIENT_FORCE_SMS_SELECTION = 19;
|
||||||
|
private static final int JOBMANAGER_STRIKES_BACK = 20;
|
||||||
|
|
||||||
private static final int DATABASE_VERSION = 19;
|
private static final int DATABASE_VERSION = 20;
|
||||||
private static final String DATABASE_NAME = "signal.db";
|
private static final String DATABASE_NAME = "signal.db";
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
@ -107,6 +109,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||||
for (String sql : SearchDatabase.CREATE_TABLE) {
|
for (String sql : SearchDatabase.CREATE_TABLE) {
|
||||||
db.execSQL(sql);
|
db.execSQL(sql);
|
||||||
}
|
}
|
||||||
|
for (String sql : JobDatabase.CREATE_TABLE) {
|
||||||
|
db.execSQL(sql);
|
||||||
|
}
|
||||||
|
|
||||||
executeStatements(db, SmsDatabase.CREATE_INDEXS);
|
executeStatements(db, SmsDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
||||||
|
@ -128,7 +133,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||||
else TextSecurePreferences.setNeedsSqlCipherMigration(context, true);
|
else TextSecurePreferences.setNeedsSqlCipherMigration(context, true);
|
||||||
|
|
||||||
if (!PreKeyMigrationHelper.migratePreKeys(context, db)) {
|
if (!PreKeyMigrationHelper.migratePreKeys(context, db)) {
|
||||||
ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob(context));
|
ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob());
|
||||||
}
|
}
|
||||||
|
|
||||||
SessionStoreMigrationHelper.migrateSessions(context, db);
|
SessionStoreMigrationHelper.migrateSessions(context, db);
|
||||||
|
@ -154,7 +159,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||||
db.execSQL("CREATE TABLE one_time_prekeys (_id INTEGER PRIMARY KEY, key_id INTEGER UNIQUE, public_key TEXT NOT NULL, private_key TEXT NOT NULL)");
|
db.execSQL("CREATE TABLE one_time_prekeys (_id INTEGER PRIMARY KEY, key_id INTEGER UNIQUE, public_key TEXT NOT NULL, private_key TEXT NOT NULL)");
|
||||||
|
|
||||||
if (!PreKeyMigrationHelper.migratePreKeys(context, db)) {
|
if (!PreKeyMigrationHelper.migratePreKeys(context, db)) {
|
||||||
ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob(context));
|
ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -407,6 +412,32 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||||
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN force_sms_selection INTEGER DEFAULT 0");
|
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN force_sms_selection INTEGER DEFAULT 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < JOBMANAGER_STRIKES_BACK) {
|
||||||
|
db.execSQL("CREATE TABLE job_spec(_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||||
|
"job_spec_id TEXT UNIQUE, " +
|
||||||
|
"factory_key TEXT, " +
|
||||||
|
"queue_key TEXT, " +
|
||||||
|
"create_time INTEGER, " +
|
||||||
|
"next_run_attempt_time INTEGER, " +
|
||||||
|
"run_attempt INTEGER, " +
|
||||||
|
"max_attempts INTEGER, " +
|
||||||
|
"max_backoff INTEGER, " +
|
||||||
|
"max_instances INTEGER, " +
|
||||||
|
"lifespan INTEGER, " +
|
||||||
|
"serialized_data TEXT, " +
|
||||||
|
"is_running INTEGER)");
|
||||||
|
|
||||||
|
db.execSQL("CREATE TABLE constraint_spec(_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||||
|
"job_spec_id TEXT, " +
|
||||||
|
"factory_key TEXT, " +
|
||||||
|
"UNIQUE(job_spec_id, factory_key))");
|
||||||
|
|
||||||
|
db.execSQL("CREATE TABLE dependency_spec(_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||||
|
"job_spec_id TEXT, " +
|
||||||
|
"depends_on_job_spec_id TEXT, " +
|
||||||
|
"UNIQUE(job_spec_id, depends_on_job_spec_id))");
|
||||||
|
}
|
||||||
|
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
} finally {
|
} finally {
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
|
|
|
@ -25,7 +25,6 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceReadReceiptUpdateJob;
|
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
|
||||||
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||||
|
@ -88,7 +87,6 @@ import dagger.Provides;
|
||||||
RetrieveProfileAvatarJob.class,
|
RetrieveProfileAvatarJob.class,
|
||||||
MultiDeviceProfileKeyUpdateJob.class,
|
MultiDeviceProfileKeyUpdateJob.class,
|
||||||
SendReadReceiptJob.class,
|
SendReadReceiptJob.class,
|
||||||
MultiDeviceReadReceiptUpdateJob.class,
|
|
||||||
AppProtectionPreferenceFragment.class,
|
AppProtectionPreferenceFragment.class,
|
||||||
FcmService.class,
|
FcmService.class,
|
||||||
RotateCertificateJob.class,
|
RotateCertificateJob.class,
|
||||||
|
|
|
@ -10,7 +10,7 @@ import com.google.firebase.messaging.RemoteMessage;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||||
import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement;
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
@ -21,7 +21,6 @@ import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.WakeLockUtil;
|
import org.thoughtcrime.securesms.util.WakeLockUtil;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||||
import org.whispersystems.signalservice.internal.util.Util;
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
|
||||||
|
@ -64,7 +63,7 @@ public class FcmService extends FirebaseMessagingService implements InjectableTy
|
||||||
|
|
||||||
ApplicationContext.getInstance(getApplicationContext())
|
ApplicationContext.getInstance(getApplicationContext())
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new FcmRefreshJob(getApplicationContext()));
|
.add(new FcmRefreshJob());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleReceivedNotification(Context context) {
|
private void handleReceivedNotification(Context context) {
|
||||||
|
@ -78,7 +77,7 @@ public class FcmService extends FirebaseMessagingService implements InjectableTy
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
PowerManager powerManager = ServiceUtil.getPowerManager(getApplicationContext());
|
PowerManager powerManager = ServiceUtil.getPowerManager(getApplicationContext());
|
||||||
boolean doze = PowerManagerCompat.isDeviceIdleMode(powerManager);
|
boolean doze = PowerManagerCompat.isDeviceIdleMode(powerManager);
|
||||||
boolean network = new NetworkRequirement(context).isPresent();
|
boolean network = new NetworkConstraint.Factory(ApplicationContext.getInstance(context)).create().isMet();
|
||||||
|
|
||||||
final Object foregroundLock = new Object();
|
final Object foregroundLock = new Object();
|
||||||
final AtomicBoolean foregroundRunning = new AtomicBoolean(false);
|
final AtomicBoolean foregroundRunning = new AtomicBoolean(false);
|
||||||
|
|
|
@ -166,7 +166,7 @@ public class GroupMessageProcessor {
|
||||||
if (record.getMembers().contains(Address.fromExternal(context, content.getSender()))) {
|
if (record.getMembers().contains(Address.fromExternal(context, content.getSender()))) {
|
||||||
ApplicationContext.getInstance(context)
|
ApplicationContext.getInstance(context)
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new PushGroupUpdateJob(context, content.getSender(), group.getGroupId()));
|
.add(new PushGroupUpdateJob(content.getSender(), group.getGroupId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -204,7 +204,7 @@ public class GroupMessageProcessor {
|
||||||
{
|
{
|
||||||
if (group.getAvatar().isPresent()) {
|
if (group.getAvatar().isPresent()) {
|
||||||
ApplicationContext.getInstance(context).getJobManager()
|
ApplicationContext.getInstance(context).getJobManager()
|
||||||
.add(new AvatarDownloadJob(context, group.getGroupId()));
|
.add(new AvatarDownloadJob(group.getGroupId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
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 android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
|
import org.thoughtcrime.securesms.BuildConfig;
|
||||||
|
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.");
|
||||||
|
ApplicationContext.getInstance(context).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.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,45 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.jobmanager;
|
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
|
||||||
|
|
||||||
public class ChainParameters {
|
|
||||||
|
|
||||||
private final String groupId;
|
|
||||||
private final boolean ignoreDuplicates;
|
|
||||||
|
|
||||||
private ChainParameters(@NonNull String groupId, boolean ignoreDuplicates) {
|
|
||||||
this.groupId = groupId;
|
|
||||||
this.ignoreDuplicates = ignoreDuplicates;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<String> getGroupId() {
|
|
||||||
return Optional.fromNullable(groupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean shouldIgnoreDuplicates() {
|
|
||||||
return ignoreDuplicates;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Builder {
|
|
||||||
|
|
||||||
private String groupId;
|
|
||||||
private boolean ignoreDuplicates;
|
|
||||||
|
|
||||||
public Builder setGroupId(@Nullable String groupId) {
|
|
||||||
this.groupId = groupId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder ignoreDuplicates(boolean ignore) {
|
|
||||||
this.ignoreDuplicates = ignore;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChainParameters build() {
|
|
||||||
return new ChainParameters(groupId, ignoreDuplicates);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager;
|
||||||
|
|
||||||
|
import android.support.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 android.support.annotation.NonNull;
|
||||||
|
import android.support.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 android.support.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 android.support.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 android.support.annotation.NonNull;
|
||||||
|
import android.support.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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,11 +14,11 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms.jobmanager.dependencies;
|
package org.thoughtcrime.securesms.jobmanager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface responsible for injecting dependencies into Jobs.
|
* Interface responsible for injecting dependencies into Jobs.
|
||||||
*/
|
*/
|
||||||
public interface DependencyInjector {
|
public interface DependencyInjector {
|
||||||
public void injectDependencies(Object object);
|
void injectDependencies(Object object);
|
||||||
}
|
}
|
|
@ -1,30 +0,0 @@
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
public class EncryptionKeys {
|
|
||||||
|
|
||||||
private transient final byte[] encoded;
|
|
||||||
|
|
||||||
public EncryptionKeys(byte[] encoded) {
|
|
||||||
this.encoded = encoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getEncoded() {
|
|
||||||
return encoded;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager;
|
||||||
|
|
||||||
|
import android.support.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 android.support.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,244 +1,281 @@
|
||||||
package org.thoughtcrime.securesms.jobmanager;
|
package org.thoughtcrime.securesms.jobmanager;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.WorkerThread;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
|
||||||
import org.thoughtcrime.securesms.R;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement;
|
|
||||||
import org.thoughtcrime.securesms.jobs.requirements.SqlCipherMigrationRequirement;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.util.LinkedList;
|
||||||
import java.util.Collections;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import androidx.work.Data;
|
/**
|
||||||
import androidx.work.ListenableWorker.Result;
|
* A durable unit of work.
|
||||||
import androidx.work.Worker;
|
*
|
||||||
import androidx.work.WorkerParameters;
|
* 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 {
|
||||||
|
|
||||||
public abstract class Job extends Worker implements Serializable {
|
private static final String TAG = Log.tag(Job.class);
|
||||||
|
|
||||||
private static final long serialVersionUID = -4658540468214421276L;
|
private final Parameters parameters;
|
||||||
|
|
||||||
private static final String TAG = Job.class.getSimpleName();
|
private String id;
|
||||||
|
private int runAttempt;
|
||||||
|
private long nextRunAttemptTime;
|
||||||
|
|
||||||
private static final WorkLockManager WORK_LOCK_MANAGER = new WorkLockManager();
|
protected Context context;
|
||||||
|
|
||||||
static final String KEY_RETRY_COUNT = "Job_retry_count";
|
public Job(@NonNull Parameters parameters) {
|
||||||
static final String KEY_RETRY_UNTIL = "Job_retry_until";
|
|
||||||
static final String KEY_SUBMIT_TIME = "Job_submit_time";
|
|
||||||
static final String KEY_FAILED = "Job_failed";
|
|
||||||
static final String KEY_REQUIRES_NETWORK = "Job_requires_network";
|
|
||||||
static final String KEY_REQUIRES_SQLCIPHER = "Job_requires_sqlcipher";
|
|
||||||
|
|
||||||
private JobParameters parameters;
|
|
||||||
|
|
||||||
public Job(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
|
||||||
super(context, workerParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invoked when a job is first created in our own codebase.
|
|
||||||
*/
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
protected Job(@NonNull Context context, @Nullable JobParameters parameters) {
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
super(context, new WorkerParameters(null, null, Collections.emptySet(), null, 0, null, null, null));
|
|
||||||
this.parameters = parameters;
|
this.parameters = parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public final String getId() {
|
||||||
public @NonNull Result doWork() {
|
return id;
|
||||||
log("doWork()" + logSuffix());
|
|
||||||
|
|
||||||
try (WorkLockManager.WorkLock workLock = WORK_LOCK_MANAGER.acquire(getId())) {
|
|
||||||
Result result = workLock.getResult();
|
|
||||||
|
|
||||||
if (result == null) {
|
|
||||||
result = doWorkInternal();
|
|
||||||
workLock.setResult(result);
|
|
||||||
} else {
|
|
||||||
log("Using result from preempted run (" + result + ")." + logSuffix());
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NonNull Result doWorkInternal() {
|
public final @NonNull Parameters getParameters() {
|
||||||
Data data = getInputData();
|
return parameters;
|
||||||
|
|
||||||
log("doWorkInternal()" + logSuffix());
|
|
||||||
|
|
||||||
ApplicationContext.getInstance(getApplicationContext()).injectDependencies(this);
|
|
||||||
|
|
||||||
if (this instanceof ContextDependent) {
|
|
||||||
((ContextDependent)this).setContext(getApplicationContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
initialize(new SafeData(data));
|
|
||||||
|
|
||||||
if (data.getBoolean(KEY_FAILED, false)) {
|
|
||||||
warn("Failing due to a failure earlier in the chain." + logSuffix());
|
|
||||||
return cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!withinRetryLimits(data)) {
|
|
||||||
warn("Failing after hitting the retry limit." + logSuffix());
|
|
||||||
return cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!requirementsMet(data)) {
|
|
||||||
log("Retrying due to unmet requirements." + logSuffix());
|
|
||||||
return retry();
|
|
||||||
}
|
|
||||||
|
|
||||||
onRun();
|
|
||||||
|
|
||||||
log("Successfully completed." + logSuffix());
|
|
||||||
return success();
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (onShouldRetry(e)) {
|
|
||||||
log("Retrying after a retryable exception." + logSuffix(), e);
|
|
||||||
return retry();
|
|
||||||
}
|
|
||||||
warn("Failing due to an exception." + logSuffix(), e);
|
|
||||||
return cancel();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public final int getRunAttempt() {
|
||||||
public void onStopped() {
|
return runAttempt;
|
||||||
log("onStopped()" + logSuffix());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final void onSubmit(@NonNull Context context, @NonNull UUID id) {
|
public final long getNextRunAttemptTime() {
|
||||||
Log.i(TAG, buildLog(id, "onSubmit() network: " + (new NetworkRequirement(getApplicationContext()).isPresent())));
|
return nextRunAttemptTime;
|
||||||
|
}
|
||||||
|
|
||||||
if (this instanceof ContextDependent) {
|
/**
|
||||||
((ContextDependent) this).setContext(context);
|
* 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.
|
||||||
|
*/
|
||||||
|
protected final void setContext(@NonNull Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Should only be invoked by {@link JobController} */
|
||||||
|
final void setId(@NonNull String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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();
|
onAdded();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called after a run has finished and we've determined a retry is required, but before the next
|
* Called when the job is first submitted to the {@link JobManager}.
|
||||||
* attempt is run.
|
|
||||||
*/
|
*/
|
||||||
protected void onRetry() { }
|
@WorkerThread
|
||||||
|
public void onAdded() {
|
||||||
/**
|
|
||||||
* Called after a job has been added to the JobManager queue. Invoked off the main thread, so its
|
|
||||||
* safe to do longer-running work. However, work should finish relatively quickly, as it will
|
|
||||||
* block the submission of future tasks.
|
|
||||||
*/
|
|
||||||
protected void onAdded() { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All instance state needs to be persisted in the provided {@link Data.Builder} so that it can
|
|
||||||
* be restored in {@link #initialize(SafeData)}.
|
|
||||||
* @param dataBuilder The builder where you put your state.
|
|
||||||
* @return The result of {@code dataBuilder.build()}.
|
|
||||||
*/
|
|
||||||
protected abstract @NonNull Data serialize(@NonNull Data.Builder dataBuilder);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore all of your instance state from the provided {@link Data}. It should contain all of
|
|
||||||
* the data put in during {@link #serialize(Data.Builder)}.
|
|
||||||
* @param data Where your data is stored.
|
|
||||||
*/
|
|
||||||
protected abstract void initialize(@NonNull SafeData data);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to actually execute the job.
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public abstract void onRun() throws Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called if a job fails to run (onShouldRetry returned false, or the number of retries exceeded
|
|
||||||
* the job's configured retry count.
|
|
||||||
*/
|
|
||||||
protected abstract void onCanceled();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If onRun() throws an exception, this method will be called to determine whether the
|
|
||||||
* job should be retried.
|
|
||||||
*
|
|
||||||
* @param exception The exception onRun() threw.
|
|
||||||
* @return true if onRun() should be called again, false otherwise.
|
|
||||||
*/
|
|
||||||
protected abstract boolean onShouldRetry(Exception exception);
|
|
||||||
|
|
||||||
@Nullable JobParameters getJobParameters() {
|
|
||||||
return parameters;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result success() {
|
/**
|
||||||
return Result.success();
|
* Called after a job has run and its determined that a retry is required.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
public void onRetry() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result retry() {
|
/**
|
||||||
onRetry();
|
* Serialize your job state so that it can be recreated in the future.
|
||||||
return Result.retry();
|
*/
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result cancel() {
|
public enum Result {
|
||||||
onCanceled();
|
SUCCESS, FAILURE, RETRY
|
||||||
return Result.success(new Data.Builder().putBoolean(KEY_FAILED, true).build());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean requirementsMet(@NonNull Data data) {
|
public static final class Parameters {
|
||||||
boolean met = true;
|
|
||||||
|
|
||||||
if (data.getBoolean(KEY_REQUIRES_SQLCIPHER, false)) {
|
public static final int IMMORTAL = -1;
|
||||||
met &= new SqlCipherMigrationRequirement(getApplicationContext()).isPresent();
|
public static final int UNLIMITED = -1;
|
||||||
|
|
||||||
|
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(long createTime,
|
||||||
|
long lifespan,
|
||||||
|
int maxAttempts,
|
||||||
|
long maxBackoff,
|
||||||
|
int maxInstances,
|
||||||
|
@Nullable String queue,
|
||||||
|
@NonNull List<String> constraintKeys)
|
||||||
|
{
|
||||||
|
this.createTime = createTime;
|
||||||
|
this.lifespan = lifespan;
|
||||||
|
this.maxAttempts = maxAttempts;
|
||||||
|
this.maxBackoff = maxBackoff;
|
||||||
|
this.maxInstances = maxInstances;
|
||||||
|
this.queue = queue;
|
||||||
|
this.constraintKeys = constraintKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
return met;
|
public long getCreateTime() {
|
||||||
}
|
return createTime;
|
||||||
|
|
||||||
private boolean withinRetryLimits(@NonNull Data data) {
|
|
||||||
int retryCount = data.getInt(KEY_RETRY_COUNT, 0);
|
|
||||||
long retryUntil = data.getLong(KEY_RETRY_UNTIL, 0);
|
|
||||||
|
|
||||||
if (retryCount > 0) {
|
|
||||||
return getRunAttemptCount() <= retryCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return System.currentTimeMillis() < retryUntil;
|
public long getLifespan() {
|
||||||
}
|
return lifespan;
|
||||||
|
}
|
||||||
|
|
||||||
private void log(@NonNull String message) {
|
public int getMaxAttempts() {
|
||||||
log(message, null);
|
return maxAttempts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void log(@NonNull String message, @Nullable Throwable t) {
|
public long getMaxBackoff() {
|
||||||
Log.i(TAG, buildLog(getId(), message), t);
|
return maxBackoff;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void warn(@NonNull String message) {
|
public int getMaxInstances() {
|
||||||
warn(message, null);
|
return maxInstances;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void warn(@NonNull String message, @Nullable Throwable t) {
|
public @Nullable String getQueue() {
|
||||||
Log.w(TAG, buildLog(getId(), message), t);
|
return queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildLog(@NonNull UUID id, @NonNull String message) {
|
public List<String> getConstraintKeys() {
|
||||||
return "[" + id + "] " + getClass().getSimpleName() + " :: " + message;
|
return constraintKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String logSuffix() {
|
|
||||||
long timeSinceSubmission = System.currentTimeMillis() - getInputData().getLong(KEY_SUBMIT_TIME, 0);
|
public static final class Builder {
|
||||||
return " (Time since submission: " + timeSinceSubmission + " ms, Run attempt: " + getRunAttemptCount() + ", isStopped: " + isStopped() + ")";
|
|
||||||
|
private long createTime = System.currentTimeMillis();
|
||||||
|
private long maxBackoff = TimeUnit.SECONDS.toMillis(30);
|
||||||
|
private long lifespan = IMMORTAL;
|
||||||
|
private int maxAttempts = 1;
|
||||||
|
private int maxInstances = UNLIMITED;
|
||||||
|
private String queue = null;
|
||||||
|
private List<String> constraintKeys = new LinkedList<>();
|
||||||
|
|
||||||
|
/** 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(createTime, lifespan, maxAttempts, maxBackoff, maxInstances, queue, constraintKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,353 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.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 DependencyInjector dependencyInjector;
|
||||||
|
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 DependencyInjector dependencyInjector,
|
||||||
|
@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.dependencyInjector = dependencyInjector;
|
||||||
|
this.scheduler = scheduler;
|
||||||
|
this.debouncer = debouncer;
|
||||||
|
this.callback = callback;
|
||||||
|
this.runningJobs = new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
synchronized void init() {
|
||||||
|
jobStorage.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);
|
||||||
|
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());
|
||||||
|
|
||||||
|
jobStorage.updateJobAfterRetry(job.getId(), false, nextRunAttempt, nextRunAttemptTime);
|
||||||
|
|
||||||
|
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());
|
||||||
|
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());
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
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) {
|
||||||
|
String id = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
job.setId(id);
|
||||||
|
job.setRunAttempt(0);
|
||||||
|
|
||||||
|
JobSpec jobSpec = new JobSpec(job.getId(),
|
||||||
|
job.getFactoryKey(),
|
||||||
|
job.getParameters().getQueue(),
|
||||||
|
job.getParameters().getCreateTime(),
|
||||||
|
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);
|
||||||
|
Data data = dataSerializer.deserialize(jobSpec.getSerializedData());
|
||||||
|
Job job = jobInstantiator.instantiate(jobSpec.getFactoryKey(), parameters, data);
|
||||||
|
|
||||||
|
job.setId(jobSpec.getId());
|
||||||
|
job.setRunAttempt(jobSpec.getRunAttempt());
|
||||||
|
job.setNextRunAttemptTime(jobSpec.getNextRunAttemptTime());
|
||||||
|
job.setContext(application);
|
||||||
|
|
||||||
|
dependencyInjector.injectDependencies(job);
|
||||||
|
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull Job.Parameters buildJobParameters(@NonNull JobSpec jobSpec, @NonNull List<ConstraintSpec> constraintSpecs) {
|
||||||
|
return new Job.Parameters.Builder()
|
||||||
|
.setCreateTime(jobSpec.getCreateTime())
|
||||||
|
.setLifespan(jobSpec.getLifespan())
|
||||||
|
.setMaxAttempts(jobSpec.getMaxAttempts())
|
||||||
|
.setQueue(jobSpec.getQueueKey())
|
||||||
|
.setConstraints(Stream.of(constraintSpecs).map(ConstraintSpec::getFactoryKey).toList())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long calculateNextRunAttemptTime(long currentTime, int nextAttempt, long maxBackoff) {
|
||||||
|
return currentTime + Math.min((long) Math.pow(2, nextAttempt) * 1000, maxBackoff);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
void onEmpty();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager;
|
||||||
|
|
||||||
|
import android.support.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,24 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
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("[%s][%s]%s %s (Time Since Submission: %d ms, Lifespan: %s, Run Attempt: %d/%s)",
|
||||||
|
id, job.getClass().getSimpleName(), tag, event, timeSinceSubmission, lifespan, runAttempt, maxAttempts);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,138 +1,181 @@
|
||||||
package org.thoughtcrime.securesms.jobmanager;
|
package org.thoughtcrime.securesms.jobmanager;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.app.Application;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import org.thoughtcrime.securesms.jobmanager.impl.DefaultExecutorFactory;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.migration.WorkManagerMigrator;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.util.Debouncer;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
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.ExecutionException;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import androidx.work.BackoffPolicy;
|
/**
|
||||||
import androidx.work.Constraints;
|
* Allows the scheduling of durable jobs that will be run as early as possible.
|
||||||
import androidx.work.Data;
|
*/
|
||||||
import androidx.work.ExistingWorkPolicy;
|
public class JobManager implements ConstraintObserver.Notifier {
|
||||||
import androidx.work.NetworkType;
|
|
||||||
import androidx.work.OneTimeWorkRequest;
|
|
||||||
import androidx.work.WorkContinuation;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
|
|
||||||
public class JobManager {
|
|
||||||
|
|
||||||
private static final String TAG = JobManager.class.getSimpleName();
|
private static final String TAG = JobManager.class.getSimpleName();
|
||||||
|
|
||||||
private static final Constraints NETWORK_CONSTRAINT = new Constraints.Builder()
|
private final ExecutorService executor;
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
private final JobController jobController;
|
||||||
.build();
|
private final JobRunner[] jobRunners;
|
||||||
|
|
||||||
private final Executor executor = Executors.newSingleThreadExecutor();
|
private final Set<EmptyQueueListener> emptyQueueListeners = new CopyOnWriteArraySet<>();
|
||||||
|
|
||||||
private final Context context;
|
public JobManager(@NonNull Application application, @NonNull Configuration configuration) {
|
||||||
private final WorkManager workManager;
|
this.executor = configuration.getExecutorFactory().newSingleThreadExecutor("JobManager");
|
||||||
|
this.jobRunners = new JobRunner[configuration.getJobThreadCount()];
|
||||||
|
this.jobController = new JobController(application,
|
||||||
|
configuration.getJobStorage(),
|
||||||
|
configuration.getJobInstantiator(),
|
||||||
|
configuration.getConstraintFactories(),
|
||||||
|
configuration.getDataSerializer(),
|
||||||
|
configuration.getDependencyInjector(),
|
||||||
|
Build.VERSION.SDK_INT < 26 ? new AlarmManagerScheduler(application)
|
||||||
|
: new CompositeScheduler(new InAppScheduler(this), new JobSchedulerScheduler(application)),
|
||||||
|
new Debouncer(500),
|
||||||
|
this::onEmptyQueue);
|
||||||
|
|
||||||
public JobManager(@NonNull Context context, @NonNull WorkManager workManager) {
|
|
||||||
this.context = context;
|
|
||||||
this.workManager = workManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Chain startChain(@NonNull Job job) {
|
|
||||||
return startChain(Collections.singletonList(job));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Chain startChain(@NonNull List<? extends Job> jobs) {
|
|
||||||
return new Chain(jobs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void add(Job job) {
|
|
||||||
JobParameters jobParameters = job.getJobParameters();
|
|
||||||
|
|
||||||
if (jobParameters == null) {
|
|
||||||
throw new IllegalStateException("Jobs must have JobParameters at this stage. (" + job.getClass().getSimpleName() + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
startChain(job).enqueue(jobParameters.getSoloChainParameters());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void enqueueChain(@NonNull Chain chain, @NonNull ChainParameters chainParameters) {
|
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
try {
|
if (WorkManagerMigrator.needsMigration(application)) {
|
||||||
workManager.pruneWork().getResult().get();
|
Log.i(TAG, "Detected an old WorkManager database. Migrating.");
|
||||||
} catch (ExecutionException | InterruptedException e) {
|
WorkManagerMigrator.migrate(application, configuration.getJobStorage(), configuration.getDataSerializer());
|
||||||
Log.w(TAG, "Failed to prune work.", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<List<Job>> jobListChain = chain.getJobListChain();
|
jobController.init();
|
||||||
List<List<OneTimeWorkRequest>> requestListChain = Stream.of(jobListChain)
|
|
||||||
.filter(jobList -> !jobList.isEmpty())
|
|
||||||
.map(jobList -> Stream.of(jobList).map(this::toWorkRequest).toList())
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (jobListChain.isEmpty()) {
|
for (int i = 0; i < jobRunners.length; i++) {
|
||||||
throw new IllegalStateException("Enqueued an empty chain.");
|
jobRunners[i] = new JobRunner(application, i + 1, jobController);
|
||||||
|
jobRunners[i].start();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < jobListChain.size(); i++) {
|
for (ConstraintObserver constraintObserver : configuration.getConstraintObservers()) {
|
||||||
for (int j = 0; j < jobListChain.get(i).size(); j++) {
|
constraintObserver.register(this);
|
||||||
jobListChain.get(i).get(j).onSubmit(context, requestListChain.get(i).get(j).getId());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
WorkContinuation continuation;
|
if (Build.VERSION.SDK_INT < 26) {
|
||||||
|
application.startService(new Intent(application, KeepAliveService.class));
|
||||||
if (chainParameters.getGroupId().isPresent()) {
|
|
||||||
ExistingWorkPolicy policy = chainParameters.shouldIgnoreDuplicates() ? ExistingWorkPolicy.KEEP : ExistingWorkPolicy.APPEND;
|
|
||||||
continuation = workManager.beginUniqueWork(chainParameters.getGroupId().get(), policy, requestListChain.get(0));
|
|
||||||
} else {
|
|
||||||
continuation = workManager.beginWith(requestListChain.get(0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 1; i < requestListChain.size(); i++) {
|
wakeUp();
|
||||||
continuation = continuation.then(requestListChain.get(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
continuation.enqueue();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private OneTimeWorkRequest toWorkRequest(@NonNull Job job) {
|
/**
|
||||||
JobParameters jobParameters = job.getJobParameters();
|
* Enqueues a single job to be run.
|
||||||
|
*/
|
||||||
if (jobParameters == null) {
|
public void add(@NonNull Job job) {
|
||||||
throw new IllegalStateException("Jobs must have JobParameters at this stage. (" + job.getClass().getSimpleName() + ")");
|
new Chain(this, Collections.singletonList(job)).enqueue();
|
||||||
}
|
|
||||||
|
|
||||||
Data.Builder dataBuilder = new Data.Builder().putInt(Job.KEY_RETRY_COUNT, jobParameters.getRetryCount())
|
|
||||||
.putLong(Job.KEY_RETRY_UNTIL, jobParameters.getRetryUntil())
|
|
||||||
.putLong(Job.KEY_SUBMIT_TIME, System.currentTimeMillis())
|
|
||||||
.putBoolean(Job.KEY_REQUIRES_NETWORK, jobParameters.requiresNetwork())
|
|
||||||
.putBoolean(Job.KEY_REQUIRES_SQLCIPHER, jobParameters.requiresSqlCipher());
|
|
||||||
Data data = job.serialize(dataBuilder);
|
|
||||||
|
|
||||||
OneTimeWorkRequest.Builder requestBuilder = new OneTimeWorkRequest.Builder(job.getClass())
|
|
||||||
.setInputData(data)
|
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS);
|
|
||||||
|
|
||||||
if (jobParameters.requiresNetwork()) {
|
|
||||||
requestBuilder.setConstraints(NETWORK_CONSTRAINT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return requestBuilder.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Chain {
|
/**
|
||||||
|
* 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));
|
||||||
|
}
|
||||||
|
|
||||||
private final List<List<Job>> jobs = new LinkedList<>();
|
/**
|
||||||
|
* 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 to 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) {
|
||||||
|
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<>();
|
||||||
|
|
||||||
private Chain(@NonNull List<? extends Job> jobs) {
|
|
||||||
this.jobs.add(new ArrayList<>(jobs));
|
this.jobs.add(new ArrayList<>(jobs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,16 +184,146 @@ public class JobManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Chain then(@NonNull List<Job> jobs) {
|
public Chain then(@NonNull List<Job> jobs) {
|
||||||
this.jobs.add(new ArrayList<>(jobs));
|
if (!jobs.isEmpty()) {
|
||||||
|
this.jobs.add(new ArrayList<>(jobs));
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void enqueue(@NonNull ChainParameters chainParameters) {
|
public void enqueue() {
|
||||||
enqueueChain(this, chainParameters);
|
jobManager.enqueueChain(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<List<Job>> getJobListChain() {
|
private List<List<Job>> getJobListChain() {
|
||||||
return jobs;
|
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 DependencyInjector dependencyInjector;
|
||||||
|
|
||||||
|
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 DependencyInjector dependencyInjector)
|
||||||
|
{
|
||||||
|
this.executorFactory = executorFactory;
|
||||||
|
this.jobThreadCount = jobThreadCount;
|
||||||
|
this.jobInstantiator = jobInstantiator;
|
||||||
|
this.constraintInstantiator = constraintInstantiator;
|
||||||
|
this.constraintObservers = constraintObservers;
|
||||||
|
this.dataSerializer = dataSerializer;
|
||||||
|
this.jobStorage = jobStorage;
|
||||||
|
this.dependencyInjector = dependencyInjector;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 DependencyInjector getDependencyInjector() {
|
||||||
|
return dependencyInjector;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 DependencyInjector dependencyInjector = o -> { /*noop*/ };
|
||||||
|
|
||||||
|
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 setDependencyInjector(@NonNull DependencyInjector dependencyInjector) {
|
||||||
|
this.dependencyInjector = dependencyInjector;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull Configuration build() {
|
||||||
|
return new Configuration(jobThreadCount,
|
||||||
|
executorFactory,
|
||||||
|
new JobInstantiator(jobFactories),
|
||||||
|
new ConstraintInstantiator(constraintFactories),
|
||||||
|
new ArrayList<>(constraintObservers),
|
||||||
|
dataSerializer,
|
||||||
|
jobStorage,
|
||||||
|
dependencyInjector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,202 +0,0 @@
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.requirements.NetworkBackoffRequirement;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.requirements.Requirement;
|
|
||||||
import org.thoughtcrime.securesms.jobs.requirements.NetworkOrServiceRequirement;
|
|
||||||
import org.thoughtcrime.securesms.jobs.requirements.SqlCipherMigrationRequirement;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The set of parameters that describe a {@link org.thoughtcrime.securesms.jobmanager.Job}.
|
|
||||||
*/
|
|
||||||
public class JobParameters implements Serializable {
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 4880456378402584584L;
|
|
||||||
|
|
||||||
private final List<Requirement> requirements;
|
|
||||||
private final boolean requiresNetwork;
|
|
||||||
private final boolean requiresSqlCipher;
|
|
||||||
private final int retryCount;
|
|
||||||
private final long retryUntil;
|
|
||||||
private final String groupId;
|
|
||||||
private final boolean ignoreDuplicates;
|
|
||||||
|
|
||||||
private JobParameters(String groupId,
|
|
||||||
boolean ignoreDuplicates,
|
|
||||||
boolean requiresNetwork,
|
|
||||||
boolean requiresSqlCipher,
|
|
||||||
int retryCount,
|
|
||||||
long retryUntil)
|
|
||||||
{
|
|
||||||
this.groupId = groupId;
|
|
||||||
this.ignoreDuplicates = ignoreDuplicates;
|
|
||||||
this.requirements = Collections.emptyList();
|
|
||||||
this.requiresNetwork = requiresNetwork;
|
|
||||||
this.requiresSqlCipher = requiresSqlCipher;
|
|
||||||
this.retryCount = retryCount;
|
|
||||||
this.retryUntil = retryUntil;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean shouldIgnoreDuplicates() {
|
|
||||||
return ignoreDuplicates;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean requiresNetwork() {
|
|
||||||
return requiresNetwork || hasNetworkRequirement(requirements);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean requiresSqlCipher() {
|
|
||||||
return requiresSqlCipher || hasSqlCipherRequirement(requirements);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasNetworkRequirement(List<Requirement> requirements) {
|
|
||||||
if (requirements == null || requirements.size() == 0) return false;
|
|
||||||
|
|
||||||
for (Requirement requirement : requirements) {
|
|
||||||
if (requirement instanceof NetworkRequirement ||
|
|
||||||
requirement instanceof NetworkOrServiceRequirement ||
|
|
||||||
requirement instanceof NetworkBackoffRequirement)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasSqlCipherRequirement(List<Requirement> requirements) {
|
|
||||||
if (requirements == null || requirements.size() == 0) return false;
|
|
||||||
|
|
||||||
for (Requirement requirement : requirements) {
|
|
||||||
if (requirement instanceof SqlCipherMigrationRequirement) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getRetryCount() {
|
|
||||||
return retryCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getRetryUntil() {
|
|
||||||
return retryUntil;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChainParameters getSoloChainParameters() {
|
|
||||||
return new ChainParameters.Builder()
|
|
||||||
.setGroupId(groupId)
|
|
||||||
.ignoreDuplicates(ignoreDuplicates)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return a builder used to construct JobParameters.
|
|
||||||
*/
|
|
||||||
public static Builder newBuilder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getGroupId() {
|
|
||||||
return groupId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Builder {
|
|
||||||
private int retryCount = 100;
|
|
||||||
private long retryDuration = 0;
|
|
||||||
private String groupId = null;
|
|
||||||
private boolean ignoreDuplicates = false;
|
|
||||||
private boolean requiresNetwork = false;
|
|
||||||
private boolean requiresSqlCipher = false;
|
|
||||||
|
|
||||||
public Builder withNetworkRequirement() {
|
|
||||||
requiresNetwork = true;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
public Builder withSqlCipherRequirement() {
|
|
||||||
requiresSqlCipher = true;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify how many times the job should be retried if execution fails but onShouldRetry() returns
|
|
||||||
* true.
|
|
||||||
*
|
|
||||||
* @param retryCount The number of times the job should be retried.
|
|
||||||
* @return the builder.
|
|
||||||
*/
|
|
||||||
public Builder withRetryCount(int retryCount) {
|
|
||||||
this.retryCount = retryCount;
|
|
||||||
this.retryDuration = 0;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify for how long we should keep retrying this job. Ignored if retryCount is set.
|
|
||||||
* @param duration The duration (in ms) for how long we should keep retrying this job for.
|
|
||||||
* @return the builder
|
|
||||||
*/
|
|
||||||
public Builder withRetryDuration(long duration) {
|
|
||||||
this.retryDuration = duration;
|
|
||||||
this.retryCount = 0;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify a groupId the job should belong to. Jobs with the same groupId are guaranteed to be
|
|
||||||
* executed serially.
|
|
||||||
*
|
|
||||||
* @param groupId The job's groupId.
|
|
||||||
* @return the builder.
|
|
||||||
*/
|
|
||||||
public Builder withGroupId(String groupId) {
|
|
||||||
this.groupId = groupId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true, only one job with this groupId can be active at a time. If a job with the same
|
|
||||||
* groupId is already running, then subsequent jobs will be ignored silently. Only has an effect
|
|
||||||
* if a groupId has been specified via {@link #withGroupId(String)}.
|
|
||||||
* <p />
|
|
||||||
* Defaults to false.
|
|
||||||
*
|
|
||||||
* @param ignoreDuplicates Whether to ignore duplicates.
|
|
||||||
* @return the builder
|
|
||||||
*/
|
|
||||||
public Builder withDuplicatesIgnored(boolean ignoreDuplicates) {
|
|
||||||
this.ignoreDuplicates = ignoreDuplicates;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the JobParameters instance that describes a Job.
|
|
||||||
*/
|
|
||||||
public JobParameters create() {
|
|
||||||
return new JobParameters(groupId, ignoreDuplicates, requiresNetwork, requiresSqlCipher, retryCount, System.currentTimeMillis() + retryDuration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.os.PowerManager;
|
||||||
|
import android.support.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;
|
||||||
|
|
||||||
|
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("JobRunner-" + id);
|
||||||
|
|
||||||
|
this.application = application;
|
||||||
|
this.id = id;
|
||||||
|
this.jobController = jobController;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void run() {
|
||||||
|
while (true) {
|
||||||
|
Job job = jobController.pullNextEligibleJobForExecution();
|
||||||
|
Job.Result result = run(job);
|
||||||
|
|
||||||
|
jobController.onJobFinished(job);
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case SUCCESS:
|
||||||
|
jobController.onSuccess(job);
|
||||||
|
break;
|
||||||
|
case RETRY:
|
||||||
|
jobController.onRetry(job);
|
||||||
|
job.onRetry();
|
||||||
|
break;
|
||||||
|
case FAILURE:
|
||||||
|
List<Job> dependents = jobController.onFailure(job);
|
||||||
|
job.onCanceled();
|
||||||
|
Stream.of(dependents).forEach(Job::onCanceled);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 == Job.Result.RETRY && 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 == Job.Result.FAILURE) {
|
||||||
|
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,90 @@
|
||||||
|
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 android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.RequiresApi;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
|
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 = 1000;
|
||||||
|
|
||||||
|
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 = ApplicationContext.getInstance(getApplicationContext()).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,24 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.support.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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,55 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.jobmanager;
|
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
|
|
||||||
import androidx.work.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A wrapper around {@link Data} that does its best to throw an exception whenever a key isn't
|
|
||||||
* present in the {@link Data} object.
|
|
||||||
*/
|
|
||||||
public class SafeData {
|
|
||||||
|
|
||||||
private final Data data;
|
|
||||||
|
|
||||||
public SafeData(@NonNull Data data) {
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getInt(@NonNull String key) {
|
|
||||||
assertKeyPresence(key);
|
|
||||||
return data.getInt(key, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getLong(@NonNull String key) {
|
|
||||||
assertKeyPresence(key);
|
|
||||||
return data.getLong(key, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getString(@NonNull String key) {
|
|
||||||
assertKeyPresence(key);
|
|
||||||
return data.getString(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String[] getStringArray(@NonNull String key) {
|
|
||||||
assertKeyPresence(key);
|
|
||||||
return data.getStringArray(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long[] getLongArray(@NonNull String key) {
|
|
||||||
assertKeyPresence(key);
|
|
||||||
return data.getLongArray(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getBoolean(@NonNull String key) {
|
|
||||||
assertKeyPresence(key);
|
|
||||||
return data.getBoolean(key, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertKeyPresence(@NonNull String key) {
|
|
||||||
if (!data.getKeyValueMap().containsKey(key)) {
|
|
||||||
throw new IllegalStateException("Missing key: " + key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface Scheduler {
|
||||||
|
void schedule(long delay, @NonNull List<Constraint> constraints);
|
||||||
|
}
|
|
@ -1,94 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.jobmanager;
|
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.Semaphore;
|
|
||||||
|
|
||||||
import androidx.work.ListenableWorker.Result;
|
|
||||||
|
|
||||||
class WorkLockManager {
|
|
||||||
|
|
||||||
private final Map<UUID, WorkLock> locks = new HashMap<>();
|
|
||||||
|
|
||||||
WorkLock acquire(@NonNull UUID uuid) {
|
|
||||||
WorkLock workLock;
|
|
||||||
|
|
||||||
synchronized (this) {
|
|
||||||
workLock = locks.get(uuid);
|
|
||||||
|
|
||||||
if (workLock == null) {
|
|
||||||
workLock = new WorkLock(uuid);
|
|
||||||
locks.put(uuid, workLock);
|
|
||||||
}
|
|
||||||
|
|
||||||
workLock.increment();
|
|
||||||
}
|
|
||||||
|
|
||||||
workLock.getLock().acquireUninterruptibly();
|
|
||||||
|
|
||||||
return workLock;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void release(@NonNull UUID uuid) {
|
|
||||||
WorkLock lock;
|
|
||||||
|
|
||||||
synchronized (this) {
|
|
||||||
lock = locks.get(uuid);
|
|
||||||
|
|
||||||
if (lock == null) {
|
|
||||||
throw new IllegalStateException("Released a lock that was already removed from use.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lock.decrementAndGet() == 0) {
|
|
||||||
locks.remove(uuid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lock.getLock().release();
|
|
||||||
}
|
|
||||||
|
|
||||||
class WorkLock implements Closeable {
|
|
||||||
|
|
||||||
private final Semaphore lock;
|
|
||||||
private final UUID uuid;
|
|
||||||
|
|
||||||
private Result result;
|
|
||||||
private int count;
|
|
||||||
|
|
||||||
private WorkLock(@NonNull UUID uuid) {
|
|
||||||
this.uuid = uuid;
|
|
||||||
this.lock = new Semaphore(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void increment() {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int decrementAndGet() {
|
|
||||||
count--;
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NonNull Semaphore getLock() {
|
|
||||||
return lock;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setResult(@NonNull Result result) {
|
|
||||||
this.result = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable Result getResult() {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
WorkLockManager.this.release(uuid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.dependencies;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Any Job or Requirement that depends on {@link android.content.Context} can implement this
|
|
||||||
* interface to receive a Context after being deserialized.
|
|
||||||
*/
|
|
||||||
public interface ContextDependent {
|
|
||||||
public void setContext(Context context);
|
|
||||||
}
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.app.job.JobInfo;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Constraint;
|
||||||
|
import org.thoughtcrime.securesms.sms.TelephonyServiceState;
|
||||||
|
|
||||||
|
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() {
|
||||||
|
TelephonyServiceState telephonyServiceState = new TelephonyServiceState();
|
||||||
|
return telephonyServiceState.isConnected(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,38 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.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 Notifier notifier;
|
||||||
|
|
||||||
|
public 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ServiceStateListener extends PhoneStateListener {
|
||||||
|
@Override
|
||||||
|
public void onServiceStateChanged(ServiceState serviceState) {
|
||||||
|
if (serviceState.getState() == ServiceState.STATE_IN_SERVICE && notifier != null) {
|
||||||
|
notifier.onConstraintMet(REASON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||||
|
|
||||||
|
import android.support.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 android.support.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 android.support.annotation.NonNull;
|
||||||
|
import android.support.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 android.support.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 android.support.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 android.support.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 android.support.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,94 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager.migration;
|
||||||
|
|
||||||
|
import android.support.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) {
|
||||||
|
builder.putInt(entry.getKey(), (int) value);
|
||||||
|
} else if (type == int[].class) {
|
||||||
|
builder.putIntArray(entry.getKey(), (int[]) value);
|
||||||
|
} else if (type == Long.class) {
|
||||||
|
builder.putLong(entry.getKey(), (long) value);
|
||||||
|
} else if (type == long[].class) {
|
||||||
|
builder.putLongArray(entry.getKey(), (long[]) value);
|
||||||
|
} else if (type == Float.class) {
|
||||||
|
builder.putFloat(entry.getKey(), (float) value);
|
||||||
|
} else if (type == float[].class) {
|
||||||
|
builder.putFloatArray(entry.getKey(), (float[]) value);
|
||||||
|
} else if (type == Double.class) {
|
||||||
|
builder.putDouble(entry.getKey(), (double) value);
|
||||||
|
} else if (type == double[].class) {
|
||||||
|
builder.putDoubleArray(entry.getKey(), (double[]) value);
|
||||||
|
} else if (type == Boolean.class) {
|
||||||
|
builder.putBoolean(entry.getKey(), (boolean) value);
|
||||||
|
} else if (type == boolean[].class) {
|
||||||
|
builder.putBooleanArray(entry.getKey(), (boolean[]) value);
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager.migration;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.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,104 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager.migration;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.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.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.PushContentReceiveJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
|
||||||
|
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.RefreshUnidentifiedDeliveryAbilityJob;
|
||||||
|
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.class.getName(), PushContentReceiveJob.KEY);
|
||||||
|
put(PushDecryptJob.class.getName(), PushDecryptJob.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.class.getName(), RefreshUnidentifiedDeliveryAbilityJob.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.migration;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||||
|
|
||||||
|
import android.support.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: %s | factoryKey: %s", jobSpecId, factoryKey);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||||
|
|
||||||
|
import android.support.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: %s | dependsOnJobSpecId: %s", jobId, dependsOnJobId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||||
|
|
||||||
|
import android.support.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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,66 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.persistence;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.EncryptionKeys;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.util.Base64;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.ObjectInputStream;
|
|
||||||
import java.io.ObjectOutputStream;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.io.StringWriter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An implementation of {@link org.thoughtcrime.securesms.jobmanager.persistence.JobSerializer} that uses
|
|
||||||
* Java Serialization.
|
|
||||||
*
|
|
||||||
* NOTE: This {@link JobSerializer} does not support encryption. Jobs will be serialized normally,
|
|
||||||
* but any corresponding {@link Job} encryption keys will be ignored.
|
|
||||||
*/
|
|
||||||
public class JavaJobSerializer implements JobSerializer {
|
|
||||||
|
|
||||||
public JavaJobSerializer() {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String serialize(Job job) throws IOException {
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
ObjectOutputStream oos = new ObjectOutputStream(baos);
|
|
||||||
oos.writeObject(job);
|
|
||||||
|
|
||||||
return Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Job deserialize(EncryptionKeys keys, boolean encrypted, String serialized) throws IOException {
|
|
||||||
try {
|
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decode(serialized, Base64.NO_WRAP));
|
|
||||||
ObjectInputStream ois = new ObjectInputStream(bais);
|
|
||||||
|
|
||||||
return (Job)ois.readObject();
|
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
StringWriter sw = new StringWriter();
|
|
||||||
PrintWriter pw = new PrintWriter(sw);
|
|
||||||
e.printStackTrace(pw);
|
|
||||||
|
|
||||||
throw new IOException(e.getMessage() + "\n" + sw.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.persistence;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.EncryptionKeys;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A JobSerializer is responsible for serializing and deserializing persistent jobs.
|
|
||||||
*/
|
|
||||||
public interface JobSerializer {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serialize a job object into a string.
|
|
||||||
* @param job The Job to serialize.
|
|
||||||
* @return The serialized Job.
|
|
||||||
* @throws IOException if serialization fails.
|
|
||||||
*/
|
|
||||||
public String serialize(Job job) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deserialize a String into a Job.
|
|
||||||
* @param keys Optional encryption keys that could have been used.
|
|
||||||
* @param encrypted True if the job was encrypted using the encryption keys.
|
|
||||||
* @param serialized The serialized Job.
|
|
||||||
* @return The deserialized Job.
|
|
||||||
* @throws IOException If the Job deserialization fails.
|
|
||||||
*/
|
|
||||||
public Job deserialize(EncryptionKeys keys, boolean encrypted, String serialized) throws IOException;
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.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: %s | factoryKey: %s | queueKey: %s | createTime: %d | nextRunAttemptTime: %d | maxAttempts: %d | maxBackoff: %d | maxInstances: %d | lifespan: %d | isRunning: %b | data: %s",
|
||||||
|
id, factoryKey, queueKey, createTime, nextRunAttemptTime, maxAttempts, maxBackoff, maxInstances, lifespan, isRunning, serializedData);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.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);
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
void updateAllJobsToBePending();
|
||||||
|
|
||||||
|
@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();
|
||||||
|
}
|
|
@ -1,107 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.persistence;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.EncryptionKeys;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class PersistentStorage {
|
|
||||||
|
|
||||||
private static final int DATABASE_VERSION = 1;
|
|
||||||
|
|
||||||
private static final String TABLE_NAME = "queue";
|
|
||||||
private static final String ID = "_id";
|
|
||||||
private static final String ITEM = "item";
|
|
||||||
private static final String ENCRYPTED = "encrypted";
|
|
||||||
|
|
||||||
private static final String DATABASE_CREATE = String.format("CREATE TABLE %s (%s INTEGER PRIMARY KEY, %s TEXT NOT NULL, %s INTEGER DEFAULT 0);",
|
|
||||||
TABLE_NAME, ID, ITEM, ENCRYPTED);
|
|
||||||
|
|
||||||
private final DatabaseHelper databaseHelper;
|
|
||||||
private final JobSerializer jobSerializer;
|
|
||||||
|
|
||||||
public PersistentStorage(Context context, String name, JobSerializer serializer) {
|
|
||||||
this.databaseHelper = new DatabaseHelper(context, "_jobqueue-" + name);
|
|
||||||
this.jobSerializer = serializer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Job> getAllUnencrypted() {
|
|
||||||
return getJobs(null, ENCRYPTED + " = 0");
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Job> getJobs(EncryptionKeys keys, String where) {
|
|
||||||
List<Job> results = new LinkedList<>();
|
|
||||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
|
||||||
Cursor cursor = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
cursor = database.query(TABLE_NAME, null, where, null, null, null, ID + " ASC", null);
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
|
|
||||||
String item = cursor.getString(cursor.getColumnIndexOrThrow(ITEM));
|
|
||||||
boolean encrypted = cursor.getInt(cursor.getColumnIndexOrThrow(ENCRYPTED)) == 1;
|
|
||||||
|
|
||||||
try{
|
|
||||||
Job job = jobSerializer.deserialize(keys, encrypted, item);
|
|
||||||
results.add(job);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w("PersistentStore", e);
|
|
||||||
remove(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (cursor != null)
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void remove(long id) {
|
|
||||||
databaseHelper.getWritableDatabase()
|
|
||||||
.delete(TABLE_NAME, ID + " = ?", new String[] {String.valueOf(id)});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class DatabaseHelper extends SQLiteOpenHelper {
|
|
||||||
|
|
||||||
public DatabaseHelper(Context context, String name) {
|
|
||||||
super(context, name, null, DATABASE_VERSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(SQLiteDatabase db) {
|
|
||||||
db.execSQL(DATABASE_CREATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.jobmanager.requirements;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uses exponential backoff to re-schedule network jobs to be retried in the future.
|
|
||||||
*/
|
|
||||||
public class NetworkBackoffRequirement implements Requirement, ContextDependent {
|
|
||||||
|
|
||||||
private static final String TAG = NetworkBackoffRequirement.class.getSimpleName();
|
|
||||||
|
|
||||||
private static final long MAX_WAIT = TimeUnit.SECONDS.toMillis(30);
|
|
||||||
|
|
||||||
private transient Context context;
|
|
||||||
|
|
||||||
public NetworkBackoffRequirement(@NonNull Context context) {
|
|
||||||
this.context = context.getApplicationContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isPresent(@NonNull Job job) {
|
|
||||||
return new NetworkRequirement(context).isPresent() && System.currentTimeMillis() >= calculateNextRunTime(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRetry(@NonNull Job job) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setContext(Context context) {
|
|
||||||
this.context = context.getApplicationContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long calculateNextRunTime(@NonNull Job job) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.requirements;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.ConnectivityManager;
|
|
||||||
import android.net.NetworkInfo;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A requirement that is satisfied when a network connection is present.
|
|
||||||
*/
|
|
||||||
public class NetworkRequirement extends SimpleRequirement implements ContextDependent {
|
|
||||||
|
|
||||||
private transient Context context;
|
|
||||||
|
|
||||||
public NetworkRequirement(Context context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public NetworkRequirement() {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isPresent() {
|
|
||||||
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
||||||
NetworkInfo netInfo = cm.getActiveNetworkInfo();
|
|
||||||
|
|
||||||
return netInfo != null && netInfo.isConnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setContext(Context context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.requirements;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.net.ConnectivityManager;
|
|
||||||
|
|
||||||
public class NetworkRequirementProvider implements RequirementProvider {
|
|
||||||
|
|
||||||
private RequirementListener listener;
|
|
||||||
|
|
||||||
private final NetworkRequirement requirement;
|
|
||||||
|
|
||||||
public NetworkRequirementProvider(Context context) {
|
|
||||||
this.requirement = new NetworkRequirement(context);
|
|
||||||
|
|
||||||
context.getApplicationContext().registerReceiver(new BroadcastReceiver() {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (listener == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requirement.isPresent()) {
|
|
||||||
listener.onRequirementStatusChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setListener(RequirementListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.requirements;
|
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Requirement that must be satisfied before a Job can run.
|
|
||||||
*/
|
|
||||||
public interface Requirement extends Serializable {
|
|
||||||
/**
|
|
||||||
* @return true if the requirement is satisfied, false otherwise.
|
|
||||||
*/
|
|
||||||
boolean isPresent(@NonNull Job job);
|
|
||||||
|
|
||||||
void onRetry(@NonNull Job job);
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.requirements;
|
|
||||||
|
|
||||||
public interface RequirementListener {
|
|
||||||
public void onRequirementStatusChanged();
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.requirements;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies listeners when a {@link org.thoughtcrime.securesms.jobmanager.requirements.Requirement}'s
|
|
||||||
* state is likely to have changed.
|
|
||||||
*/
|
|
||||||
public interface RequirementProvider {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link org.thoughtcrime.securesms.jobmanager.requirements.RequirementListener} to call when
|
|
||||||
* a {@link org.thoughtcrime.securesms.jobmanager.requirements.Requirement}'s status is likely to
|
|
||||||
* have changed.
|
|
||||||
*
|
|
||||||
* @param listener The listener to call.
|
|
||||||
*/
|
|
||||||
public void setListener(RequirementListener listener);
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.jobmanager.requirements;
|
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
|
|
||||||
public abstract class SimpleRequirement implements Requirement {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isPresent(@NonNull Job job) {
|
|
||||||
return isPresent();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRetry(@NonNull Job job) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract boolean isPresent();
|
|
||||||
}
|
|
|
@ -1,741 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.jobmanager.util;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utilities for encoding and decoding the Base64 representation of
|
|
||||||
* binary data. See RFCs <a
|
|
||||||
* href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a
|
|
||||||
* href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>.
|
|
||||||
*/
|
|
||||||
public class Base64 {
|
|
||||||
/**
|
|
||||||
* Default values for encoder/decoder flags.
|
|
||||||
*/
|
|
||||||
public static final int DEFAULT = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encoder flag bit to omit the padding '=' characters at the end
|
|
||||||
* of the output (if any).
|
|
||||||
*/
|
|
||||||
public static final int NO_PADDING = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encoder flag bit to omit all line terminators (i.e., the output
|
|
||||||
* will be on one long line).
|
|
||||||
*/
|
|
||||||
public static final int NO_WRAP = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encoder flag bit to indicate lines should be terminated with a
|
|
||||||
* CRLF pair instead of just an LF. Has no effect if {@code
|
|
||||||
* NO_WRAP} is specified as well.
|
|
||||||
*/
|
|
||||||
public static final int CRLF = 4;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encoder/decoder flag bit to indicate using the "URL and
|
|
||||||
* filename safe" variant of Base64 (see RFC 3548 section 4) where
|
|
||||||
* {@code -} and {@code _} are used in place of {@code +} and
|
|
||||||
* {@code /}.
|
|
||||||
*/
|
|
||||||
public static final int URL_SAFE = 8;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flag to pass to {@link android.util.Base64OutputStream} to indicate that it
|
|
||||||
* should not close the output stream it is wrapping when it
|
|
||||||
* itself is closed.
|
|
||||||
*/
|
|
||||||
public static final int NO_CLOSE = 16;
|
|
||||||
|
|
||||||
// --------------------------------------------------------
|
|
||||||
// shared code
|
|
||||||
// --------------------------------------------------------
|
|
||||||
|
|
||||||
/* package */ static abstract class Coder {
|
|
||||||
public byte[] output;
|
|
||||||
public int op;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encode/decode another block of input data. this.output is
|
|
||||||
* provided by the caller, and must be big enough to hold all
|
|
||||||
* the coded data. On exit, this.opwill be set to the length
|
|
||||||
* of the coded data.
|
|
||||||
*
|
|
||||||
* @param finish true if this is the final call to process for
|
|
||||||
* this object. Will finalize the coder state and
|
|
||||||
* include any final bytes in the output.
|
|
||||||
*
|
|
||||||
* @return true if the input so far is good; false if some
|
|
||||||
* error has been detected in the input stream..
|
|
||||||
*/
|
|
||||||
public abstract boolean process(byte[] input, int offset, int len, boolean finish);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the maximum number of bytes a call to process()
|
|
||||||
* could produce for the given number of input bytes. This may
|
|
||||||
* be an overestimate.
|
|
||||||
*/
|
|
||||||
public abstract int maxOutputSize(int len);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------------
|
|
||||||
// decoding
|
|
||||||
// --------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode the Base64-encoded data in input and return the data in
|
|
||||||
* a new byte array.
|
|
||||||
*
|
|
||||||
* <p>The padding '=' characters at the end are considered optional, but
|
|
||||||
* if any are present, there must be the correct number of them.
|
|
||||||
*
|
|
||||||
* @param str the input String to decode, which is converted to
|
|
||||||
* bytes using the default charset
|
|
||||||
* @param flags controls certain features of the decoded output.
|
|
||||||
* Pass {@code DEFAULT} to decode standard Base64.
|
|
||||||
*
|
|
||||||
* @throws IllegalArgumentException if the input contains
|
|
||||||
* incorrect padding
|
|
||||||
*/
|
|
||||||
public static byte[] decode(String str, int flags) {
|
|
||||||
return decode(str.getBytes(), flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode the Base64-encoded data in input and return the data in
|
|
||||||
* a new byte array.
|
|
||||||
*
|
|
||||||
* <p>The padding '=' characters at the end are considered optional, but
|
|
||||||
* if any are present, there must be the correct number of them.
|
|
||||||
*
|
|
||||||
* @param input the input array to decode
|
|
||||||
* @param flags controls certain features of the decoded output.
|
|
||||||
* Pass {@code DEFAULT} to decode standard Base64.
|
|
||||||
*
|
|
||||||
* @throws IllegalArgumentException if the input contains
|
|
||||||
* incorrect padding
|
|
||||||
*/
|
|
||||||
public static byte[] decode(byte[] input, int flags) {
|
|
||||||
return decode(input, 0, input.length, flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode the Base64-encoded data in input and return the data in
|
|
||||||
* a new byte array.
|
|
||||||
*
|
|
||||||
* <p>The padding '=' characters at the end are considered optional, but
|
|
||||||
* if any are present, there must be the correct number of them.
|
|
||||||
*
|
|
||||||
* @param input the data to decode
|
|
||||||
* @param offset the position within the input array at which to start
|
|
||||||
* @param len the number of bytes of input to decode
|
|
||||||
* @param flags controls certain features of the decoded output.
|
|
||||||
* Pass {@code DEFAULT} to decode standard Base64.
|
|
||||||
*
|
|
||||||
* @throws IllegalArgumentException if the input contains
|
|
||||||
* incorrect padding
|
|
||||||
*/
|
|
||||||
public static byte[] decode(byte[] input, int offset, int len, int flags) {
|
|
||||||
// Allocate space for the most data the input could represent.
|
|
||||||
// (It could contain less if it contains whitespace, etc.)
|
|
||||||
Decoder decoder = new Decoder(flags, new byte[len*3/4]);
|
|
||||||
|
|
||||||
if (!decoder.process(input, offset, len, true)) {
|
|
||||||
throw new IllegalArgumentException("bad base-64");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maybe we got lucky and allocated exactly enough output space.
|
|
||||||
if (decoder.op == decoder.output.length) {
|
|
||||||
return decoder.output;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Need to shorten the array, so allocate a new one of the
|
|
||||||
// right size and copy.
|
|
||||||
byte[] temp = new byte[decoder.op];
|
|
||||||
System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
|
|
||||||
return temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* package */ static class Decoder extends Coder {
|
|
||||||
/**
|
|
||||||
* Lookup table for turning bytes into their position in the
|
|
||||||
* Base64 alphabet.
|
|
||||||
*/
|
|
||||||
private static final int DECODE[] = {
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
|
|
||||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
|
|
||||||
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
|
||||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
|
|
||||||
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
|
||||||
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode lookup table for the "web safe" variant (RFC 3548
|
|
||||||
* sec. 4) where - and _ replace + and /.
|
|
||||||
*/
|
|
||||||
private static final int DECODE_WEBSAFE[] = {
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
|
|
||||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
|
|
||||||
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
|
||||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
|
|
||||||
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
|
||||||
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Non-data values in the DECODE arrays. */
|
|
||||||
private static final int SKIP = -1;
|
|
||||||
private static final int EQUALS = -2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* States 0-3 are reading through the next input tuple.
|
|
||||||
* State 4 is having read one '=' and expecting exactly
|
|
||||||
* one more.
|
|
||||||
* State 5 is expecting no more data or padding characters
|
|
||||||
* in the input.
|
|
||||||
* State 6 is the error state; an error has been detected
|
|
||||||
* in the input and no future input can "fix" it.
|
|
||||||
*/
|
|
||||||
private int state; // state number (0 to 6)
|
|
||||||
private int value;
|
|
||||||
|
|
||||||
final private int[] alphabet;
|
|
||||||
|
|
||||||
public Decoder(int flags, byte[] output) {
|
|
||||||
this.output = output;
|
|
||||||
|
|
||||||
alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
|
|
||||||
state = 0;
|
|
||||||
value = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return an overestimate for the number of bytes {@code
|
|
||||||
* len} bytes could decode to.
|
|
||||||
*/
|
|
||||||
public int maxOutputSize(int len) {
|
|
||||||
return len * 3/4 + 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode another block of input data.
|
|
||||||
*
|
|
||||||
* @return true if the state machine is still healthy. false if
|
|
||||||
* bad base-64 data has been detected in the input stream.
|
|
||||||
*/
|
|
||||||
public boolean process(byte[] input, int offset, int len, boolean finish) {
|
|
||||||
if (this.state == 6) return false;
|
|
||||||
|
|
||||||
int p = offset;
|
|
||||||
len += offset;
|
|
||||||
|
|
||||||
// Using local variables makes the decoder about 12%
|
|
||||||
// faster than if we manipulate the member variables in
|
|
||||||
// the loop. (Even alphabet makes a measurable
|
|
||||||
// difference, which is somewhat surprising to me since
|
|
||||||
// the member variable is final.)
|
|
||||||
int state = this.state;
|
|
||||||
int value = this.value;
|
|
||||||
int op = 0;
|
|
||||||
final byte[] output = this.output;
|
|
||||||
final int[] alphabet = this.alphabet;
|
|
||||||
|
|
||||||
while (p < len) {
|
|
||||||
// Try the fast path: we're starting a new tuple and the
|
|
||||||
// next four bytes of the input stream are all data
|
|
||||||
// bytes. This corresponds to going through states
|
|
||||||
// 0-1-2-3-0. We expect to use this method for most of
|
|
||||||
// the data.
|
|
||||||
//
|
|
||||||
// If any of the next four bytes of input are non-data
|
|
||||||
// (whitespace, etc.), value will end up negative. (All
|
|
||||||
// the non-data values in decode are small negative
|
|
||||||
// numbers, so shifting any of them up and or'ing them
|
|
||||||
// together will result in a value with its top bit set.)
|
|
||||||
//
|
|
||||||
// You can remove this whole block and the output should
|
|
||||||
// be the same, just slower.
|
|
||||||
if (state == 0) {
|
|
||||||
while (p+4 <= len &&
|
|
||||||
(value = ((alphabet[input[p] & 0xff] << 18) |
|
|
||||||
(alphabet[input[p+1] & 0xff] << 12) |
|
|
||||||
(alphabet[input[p+2] & 0xff] << 6) |
|
|
||||||
(alphabet[input[p+3] & 0xff]))) >= 0) {
|
|
||||||
output[op+2] = (byte) value;
|
|
||||||
output[op+1] = (byte) (value >> 8);
|
|
||||||
output[op] = (byte) (value >> 16);
|
|
||||||
op += 3;
|
|
||||||
p += 4;
|
|
||||||
}
|
|
||||||
if (p >= len) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The fast path isn't available -- either we've read a
|
|
||||||
// partial tuple, or the next four input bytes aren't all
|
|
||||||
// data, or whatever. Fall back to the slower state
|
|
||||||
// machine implementation.
|
|
||||||
|
|
||||||
int d = alphabet[input[p++] & 0xff];
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case 0:
|
|
||||||
if (d >= 0) {
|
|
||||||
value = d;
|
|
||||||
++state;
|
|
||||||
} else if (d != SKIP) {
|
|
||||||
this.state = 6;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
if (d >= 0) {
|
|
||||||
value = (value << 6) | d;
|
|
||||||
++state;
|
|
||||||
} else if (d != SKIP) {
|
|
||||||
this.state = 6;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
if (d >= 0) {
|
|
||||||
value = (value << 6) | d;
|
|
||||||
++state;
|
|
||||||
} else if (d == EQUALS) {
|
|
||||||
// Emit the last (partial) output tuple;
|
|
||||||
// expect exactly one more padding character.
|
|
||||||
output[op++] = (byte) (value >> 4);
|
|
||||||
state = 4;
|
|
||||||
} else if (d != SKIP) {
|
|
||||||
this.state = 6;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 3:
|
|
||||||
if (d >= 0) {
|
|
||||||
// Emit the output triple and return to state 0.
|
|
||||||
value = (value << 6) | d;
|
|
||||||
output[op+2] = (byte) value;
|
|
||||||
output[op+1] = (byte) (value >> 8);
|
|
||||||
output[op] = (byte) (value >> 16);
|
|
||||||
op += 3;
|
|
||||||
state = 0;
|
|
||||||
} else if (d == EQUALS) {
|
|
||||||
// Emit the last (partial) output tuple;
|
|
||||||
// expect no further data or padding characters.
|
|
||||||
output[op+1] = (byte) (value >> 2);
|
|
||||||
output[op] = (byte) (value >> 10);
|
|
||||||
op += 2;
|
|
||||||
state = 5;
|
|
||||||
} else if (d != SKIP) {
|
|
||||||
this.state = 6;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 4:
|
|
||||||
if (d == EQUALS) {
|
|
||||||
++state;
|
|
||||||
} else if (d != SKIP) {
|
|
||||||
this.state = 6;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 5:
|
|
||||||
if (d != SKIP) {
|
|
||||||
this.state = 6;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!finish) {
|
|
||||||
// We're out of input, but a future call could provide
|
|
||||||
// more.
|
|
||||||
this.state = state;
|
|
||||||
this.value = value;
|
|
||||||
this.op = op;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Done reading input. Now figure out where we are left in
|
|
||||||
// the state machine and finish up.
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case 0:
|
|
||||||
// Output length is a multiple of three. Fine.
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
// Read one extra input byte, which isn't enough to
|
|
||||||
// make another output byte. Illegal.
|
|
||||||
this.state = 6;
|
|
||||||
return false;
|
|
||||||
case 2:
|
|
||||||
// Read two extra input bytes, enough to emit 1 more
|
|
||||||
// output byte. Fine.
|
|
||||||
output[op++] = (byte) (value >> 4);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
// Read three extra input bytes, enough to emit 2 more
|
|
||||||
// output bytes. Fine.
|
|
||||||
output[op++] = (byte) (value >> 10);
|
|
||||||
output[op++] = (byte) (value >> 2);
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
// Read one padding '=' when we expected 2. Illegal.
|
|
||||||
this.state = 6;
|
|
||||||
return false;
|
|
||||||
case 5:
|
|
||||||
// Read all the padding '='s we expected and no more.
|
|
||||||
// Fine.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = state;
|
|
||||||
this.op = op;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------------
|
|
||||||
// encoding
|
|
||||||
// --------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base64-encode the given data and return a newly allocated
|
|
||||||
* String with the result.
|
|
||||||
*
|
|
||||||
* @param input the data to encode
|
|
||||||
* @param flags controls certain features of the encoded output.
|
|
||||||
* Passing {@code DEFAULT} results in output that
|
|
||||||
* adheres to RFC 2045.
|
|
||||||
*/
|
|
||||||
public static String encodeToString(byte[] input, int flags) {
|
|
||||||
try {
|
|
||||||
return new String(encode(input, flags), "US-ASCII");
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
// US-ASCII is guaranteed to be available.
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base64-encode the given data and return a newly allocated
|
|
||||||
* String with the result.
|
|
||||||
*
|
|
||||||
* @param input the data to encode
|
|
||||||
* @param offset the position within the input array at which to
|
|
||||||
* start
|
|
||||||
* @param len the number of bytes of input to encode
|
|
||||||
* @param flags controls certain features of the encoded output.
|
|
||||||
* Passing {@code DEFAULT} results in output that
|
|
||||||
* adheres to RFC 2045.
|
|
||||||
*/
|
|
||||||
public static String encodeToString(byte[] input, int offset, int len, int flags) {
|
|
||||||
try {
|
|
||||||
return new String(encode(input, offset, len, flags), "US-ASCII");
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
// US-ASCII is guaranteed to be available.
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base64-encode the given data and return a newly allocated
|
|
||||||
* byte[] with the result.
|
|
||||||
*
|
|
||||||
* @param input the data to encode
|
|
||||||
* @param flags controls certain features of the encoded output.
|
|
||||||
* Passing {@code DEFAULT} results in output that
|
|
||||||
* adheres to RFC 2045.
|
|
||||||
*/
|
|
||||||
public static byte[] encode(byte[] input, int flags) {
|
|
||||||
return encode(input, 0, input.length, flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base64-encode the given data and return a newly allocated
|
|
||||||
* byte[] with the result.
|
|
||||||
*
|
|
||||||
* @param input the data to encode
|
|
||||||
* @param offset the position within the input array at which to
|
|
||||||
* start
|
|
||||||
* @param len the number of bytes of input to encode
|
|
||||||
* @param flags controls certain features of the encoded output.
|
|
||||||
* Passing {@code DEFAULT} results in output that
|
|
||||||
* adheres to RFC 2045.
|
|
||||||
*/
|
|
||||||
public static byte[] encode(byte[] input, int offset, int len, int flags) {
|
|
||||||
Encoder encoder = new Encoder(flags, null);
|
|
||||||
|
|
||||||
// Compute the exact length of the array we will produce.
|
|
||||||
int output_len = len / 3 * 4;
|
|
||||||
|
|
||||||
// Account for the tail of the data and the padding bytes, if any.
|
|
||||||
if (encoder.do_padding) {
|
|
||||||
if (len % 3 > 0) {
|
|
||||||
output_len += 4;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (len % 3) {
|
|
||||||
case 0: break;
|
|
||||||
case 1: output_len += 2; break;
|
|
||||||
case 2: output_len += 3; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Account for the newlines, if any.
|
|
||||||
if (encoder.do_newline && len > 0) {
|
|
||||||
output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) *
|
|
||||||
(encoder.do_cr ? 2 : 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
encoder.output = new byte[output_len];
|
|
||||||
encoder.process(input, offset, len, true);
|
|
||||||
|
|
||||||
assert encoder.op == output_len;
|
|
||||||
|
|
||||||
return encoder.output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* package */ static class Encoder extends Coder {
|
|
||||||
/**
|
|
||||||
* Emit a new line every this many output tuples. Corresponds to
|
|
||||||
* a 76-character line length (the maximum allowable according to
|
|
||||||
* <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>).
|
|
||||||
*/
|
|
||||||
public static final int LINE_GROUPS = 19;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lookup table for turning Base64 alphabet positions (6 bits)
|
|
||||||
* into output bytes.
|
|
||||||
*/
|
|
||||||
private static final byte ENCODE[] = {
|
|
||||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
|
||||||
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
|
||||||
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
|
||||||
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lookup table for turning Base64 alphabet positions (6 bits)
|
|
||||||
* into output bytes.
|
|
||||||
*/
|
|
||||||
private static final byte ENCODE_WEBSAFE[] = {
|
|
||||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
|
||||||
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
|
||||||
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
|
||||||
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
|
|
||||||
};
|
|
||||||
|
|
||||||
final private byte[] tail;
|
|
||||||
/* package */ int tailLen;
|
|
||||||
private int count;
|
|
||||||
|
|
||||||
final public boolean do_padding;
|
|
||||||
final public boolean do_newline;
|
|
||||||
final public boolean do_cr;
|
|
||||||
final private byte[] alphabet;
|
|
||||||
|
|
||||||
public Encoder(int flags, byte[] output) {
|
|
||||||
this.output = output;
|
|
||||||
|
|
||||||
do_padding = (flags & NO_PADDING) == 0;
|
|
||||||
do_newline = (flags & NO_WRAP) == 0;
|
|
||||||
do_cr = (flags & CRLF) != 0;
|
|
||||||
alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
|
|
||||||
|
|
||||||
tail = new byte[2];
|
|
||||||
tailLen = 0;
|
|
||||||
|
|
||||||
count = do_newline ? LINE_GROUPS : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return an overestimate for the number of bytes {@code
|
|
||||||
* len} bytes could encode to.
|
|
||||||
*/
|
|
||||||
public int maxOutputSize(int len) {
|
|
||||||
return len * 8/5 + 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean process(byte[] input, int offset, int len, boolean finish) {
|
|
||||||
// Using local variables makes the encoder about 9% faster.
|
|
||||||
final byte[] alphabet = this.alphabet;
|
|
||||||
final byte[] output = this.output;
|
|
||||||
int op = 0;
|
|
||||||
int count = this.count;
|
|
||||||
|
|
||||||
int p = offset;
|
|
||||||
len += offset;
|
|
||||||
int v = -1;
|
|
||||||
|
|
||||||
// First we need to concatenate the tail of the previous call
|
|
||||||
// with any input bytes available now and see if we can empty
|
|
||||||
// the tail.
|
|
||||||
|
|
||||||
switch (tailLen) {
|
|
||||||
case 0:
|
|
||||||
// There was no tail.
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
if (p+2 <= len) {
|
|
||||||
// A 1-byte tail with at least 2 bytes of
|
|
||||||
// input available now.
|
|
||||||
v = ((tail[0] & 0xff) << 16) |
|
|
||||||
((input[p++] & 0xff) << 8) |
|
|
||||||
(input[p++] & 0xff);
|
|
||||||
tailLen = 0;
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
if (p+1 <= len) {
|
|
||||||
// A 2-byte tail with at least 1 byte of input.
|
|
||||||
v = ((tail[0] & 0xff) << 16) |
|
|
||||||
((tail[1] & 0xff) << 8) |
|
|
||||||
(input[p++] & 0xff);
|
|
||||||
tailLen = 0;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (v != -1) {
|
|
||||||
output[op++] = alphabet[(v >> 18) & 0x3f];
|
|
||||||
output[op++] = alphabet[(v >> 12) & 0x3f];
|
|
||||||
output[op++] = alphabet[(v >> 6) & 0x3f];
|
|
||||||
output[op++] = alphabet[v & 0x3f];
|
|
||||||
if (--count == 0) {
|
|
||||||
if (do_cr) output[op++] = '\r';
|
|
||||||
output[op++] = '\n';
|
|
||||||
count = LINE_GROUPS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point either there is no tail, or there are fewer
|
|
||||||
// than 3 bytes of input available.
|
|
||||||
|
|
||||||
// The main loop, turning 3 input bytes into 4 output bytes on
|
|
||||||
// each iteration.
|
|
||||||
while (p+3 <= len) {
|
|
||||||
v = ((input[p] & 0xff) << 16) |
|
|
||||||
((input[p+1] & 0xff) << 8) |
|
|
||||||
(input[p+2] & 0xff);
|
|
||||||
output[op] = alphabet[(v >> 18) & 0x3f];
|
|
||||||
output[op+1] = alphabet[(v >> 12) & 0x3f];
|
|
||||||
output[op+2] = alphabet[(v >> 6) & 0x3f];
|
|
||||||
output[op+3] = alphabet[v & 0x3f];
|
|
||||||
p += 3;
|
|
||||||
op += 4;
|
|
||||||
if (--count == 0) {
|
|
||||||
if (do_cr) output[op++] = '\r';
|
|
||||||
output[op++] = '\n';
|
|
||||||
count = LINE_GROUPS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finish) {
|
|
||||||
// Finish up the tail of the input. Note that we need to
|
|
||||||
// consume any bytes in tail before any bytes
|
|
||||||
// remaining in input; there should be at most two bytes
|
|
||||||
// total.
|
|
||||||
|
|
||||||
if (p-tailLen == len-1) {
|
|
||||||
int t = 0;
|
|
||||||
v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
|
|
||||||
tailLen -= t;
|
|
||||||
output[op++] = alphabet[(v >> 6) & 0x3f];
|
|
||||||
output[op++] = alphabet[v & 0x3f];
|
|
||||||
if (do_padding) {
|
|
||||||
output[op++] = '=';
|
|
||||||
output[op++] = '=';
|
|
||||||
}
|
|
||||||
if (do_newline) {
|
|
||||||
if (do_cr) output[op++] = '\r';
|
|
||||||
output[op++] = '\n';
|
|
||||||
}
|
|
||||||
} else if (p-tailLen == len-2) {
|
|
||||||
int t = 0;
|
|
||||||
v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
|
|
||||||
(((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
|
|
||||||
tailLen -= t;
|
|
||||||
output[op++] = alphabet[(v >> 12) & 0x3f];
|
|
||||||
output[op++] = alphabet[(v >> 6) & 0x3f];
|
|
||||||
output[op++] = alphabet[v & 0x3f];
|
|
||||||
if (do_padding) {
|
|
||||||
output[op++] = '=';
|
|
||||||
}
|
|
||||||
if (do_newline) {
|
|
||||||
if (do_cr) output[op++] = '\r';
|
|
||||||
output[op++] = '\n';
|
|
||||||
}
|
|
||||||
} else if (do_newline && op > 0 && count != LINE_GROUPS) {
|
|
||||||
if (do_cr) output[op++] = '\r';
|
|
||||||
output[op++] = '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
assert tailLen == 0;
|
|
||||||
assert p == len;
|
|
||||||
} else {
|
|
||||||
// Save the leftovers in tail to be consumed on the next
|
|
||||||
// call to encodeInternal.
|
|
||||||
|
|
||||||
if (p == len-1) {
|
|
||||||
tail[tailLen++] = input[p];
|
|
||||||
} else if (p == len-2) {
|
|
||||||
tail[tailLen++] = input[p];
|
|
||||||
tail[tailLen++] = input[p+1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.op = op;
|
|
||||||
this.count = count;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Base64() { } // don't instantiate
|
|
||||||
}
|
|
|
@ -1,23 +1,22 @@
|
||||||
package org.thoughtcrime.securesms.jobs;
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.VisibleForTesting;
|
import android.support.annotation.VisibleForTesting;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
|
||||||
import org.thoughtcrime.securesms.mms.MmsException;
|
import org.thoughtcrime.securesms.mms.MmsException;
|
||||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||||
|
@ -37,11 +36,10 @@ import java.io.InputStream;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.work.Data;
|
public class AttachmentDownloadJob extends BaseJob implements InjectableType {
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
|
public static final String KEY = "AttachmentDownloadJob";
|
||||||
|
|
||||||
public class AttachmentDownloadJob extends ContextJob implements InjectableType {
|
|
||||||
private static final long serialVersionUID = 2L;
|
|
||||||
private static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
|
private static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
|
||||||
private static final String TAG = AttachmentDownloadJob.class.getSimpleName();
|
private static final String TAG = AttachmentDownloadJob.class.getSimpleName();
|
||||||
|
|
||||||
|
@ -50,22 +48,27 @@ public class AttachmentDownloadJob extends ContextJob implements InjectableType
|
||||||
private static final String KEY_PAR_UNIQUE_ID = "part_unique_id";
|
private static final String KEY_PAR_UNIQUE_ID = "part_unique_id";
|
||||||
private static final String KEY_MANUAL = "part_manual";
|
private static final String KEY_MANUAL = "part_manual";
|
||||||
|
|
||||||
@Inject transient SignalServiceMessageReceiver messageReceiver;
|
@Inject SignalServiceMessageReceiver messageReceiver;
|
||||||
|
|
||||||
private long messageId;
|
private long messageId;
|
||||||
private long partRowId;
|
private long partRowId;
|
||||||
private long partUniqueId;
|
private long partUniqueId;
|
||||||
private boolean manual;
|
private boolean manual;
|
||||||
|
|
||||||
public AttachmentDownloadJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
public AttachmentDownloadJob(long messageId, AttachmentId attachmentId, boolean manual) {
|
||||||
super(context, workerParameters);
|
this(new Job.Parameters.Builder()
|
||||||
|
.setQueue("AttachmentDownloadJob" + attachmentId.getRowId() + "-" + attachmentId.getUniqueId())
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setMaxAttempts(25)
|
||||||
|
.build(),
|
||||||
|
messageId,
|
||||||
|
attachmentId,
|
||||||
|
manual);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public AttachmentDownloadJob(Context context, long messageId, AttachmentId attachmentId, boolean manual) {
|
private AttachmentDownloadJob(@NonNull Job.Parameters parameters, long messageId, AttachmentId attachmentId, boolean manual) {
|
||||||
super(context, JobParameters.newBuilder()
|
super(parameters);
|
||||||
.withGroupId(AttachmentDownloadJob.class.getSimpleName() + attachmentId.getRowId() + "-" + attachmentId.getUniqueId())
|
|
||||||
.withNetworkRequirement()
|
|
||||||
.create());
|
|
||||||
|
|
||||||
this.messageId = messageId;
|
this.messageId = messageId;
|
||||||
this.partRowId = attachmentId.getRowId();
|
this.partRowId = attachmentId.getRowId();
|
||||||
|
@ -74,20 +77,17 @@ public class AttachmentDownloadJob extends ContextJob implements InjectableType
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize(@NonNull SafeData data) {
|
public @NonNull Data serialize() {
|
||||||
messageId = data.getLong(KEY_MESSAGE_ID);
|
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
|
||||||
partRowId = data.getLong(KEY_PART_ROW_ID);
|
.putLong(KEY_PART_ROW_ID, partRowId)
|
||||||
partUniqueId = data.getLong(KEY_PAR_UNIQUE_ID);
|
.putLong(KEY_PAR_UNIQUE_ID, partUniqueId)
|
||||||
manual = data.getBoolean(KEY_MANUAL);
|
.putBoolean(KEY_MANUAL, manual)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
|
public @NonNull String getFactoryKey() {
|
||||||
return dataBuilder.putLong(KEY_MESSAGE_ID, messageId)
|
return KEY;
|
||||||
.putLong(KEY_PART_ROW_ID, partRowId)
|
|
||||||
.putLong(KEY_PAR_UNIQUE_ID, partUniqueId)
|
|
||||||
.putBoolean(KEY_MANUAL, manual)
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -242,4 +242,13 @@ public class AttachmentDownloadJob extends ContextJob implements InjectableType
|
||||||
InvalidPartException(Exception e) {super(e);}
|
InvalidPartException(Exception e) {super(e);}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class Factory implements Job.Factory<AttachmentDownloadJob> {
|
||||||
|
@Override
|
||||||
|
public @NonNull AttachmentDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
return new AttachmentDownloadJob(parameters,
|
||||||
|
data.getLong(KEY_MESSAGE_ID),
|
||||||
|
new AttachmentId(data.getLong(KEY_PART_ROW_ID), data.getLong(KEY_PAR_UNIQUE_ID)),
|
||||||
|
data.getBoolean(KEY_MANUAL));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package org.thoughtcrime.securesms.jobs;
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
|
@ -12,8 +11,9 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||||
import org.thoughtcrime.securesms.mms.MediaStream;
|
import org.thoughtcrime.securesms.mms.MediaStream;
|
||||||
|
@ -35,10 +35,9 @@ import java.util.concurrent.TimeUnit;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.net.ssl.SSLException;
|
import javax.net.ssl.SSLException;
|
||||||
|
|
||||||
import androidx.work.Data;
|
public class AttachmentUploadJob extends BaseJob implements InjectableType {
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
|
|
||||||
public class AttachmentUploadJob extends ContextJob implements InjectableType {
|
public static final String KEY = "AttachmentUploadJob";
|
||||||
|
|
||||||
private static final String TAG = AttachmentUploadJob.class.getSimpleName();
|
private static final String TAG = AttachmentUploadJob.class.getSimpleName();
|
||||||
|
|
||||||
|
@ -48,29 +47,30 @@ public class AttachmentUploadJob extends ContextJob implements InjectableType {
|
||||||
private AttachmentId attachmentId;
|
private AttachmentId attachmentId;
|
||||||
@Inject SignalServiceMessageSender messageSender;
|
@Inject SignalServiceMessageSender messageSender;
|
||||||
|
|
||||||
public AttachmentUploadJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
public AttachmentUploadJob(AttachmentId attachmentId) {
|
||||||
super(context, workerParameters);
|
this(new Job.Parameters.Builder()
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||||
|
.setMaxAttempts(Parameters.UNLIMITED)
|
||||||
|
.build(),
|
||||||
|
attachmentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected AttachmentUploadJob(@NonNull Context context, AttachmentId attachmentId) {
|
private AttachmentUploadJob(@NonNull Job.Parameters parameters, @NonNull AttachmentId attachmentId) {
|
||||||
super(context, new JobParameters.Builder()
|
super(parameters);
|
||||||
.withNetworkRequirement()
|
|
||||||
.withRetryDuration(TimeUnit.DAYS.toMillis(1))
|
|
||||||
.create());
|
|
||||||
|
|
||||||
this.attachmentId = attachmentId;
|
this.attachmentId = attachmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize(@NonNull SafeData data) {
|
public @NonNull Data serialize() {
|
||||||
this.attachmentId = new AttachmentId(data.getLong(KEY_ROW_ID), data.getLong(KEY_UNIQUE_ID));
|
return new Data.Builder().putLong(KEY_ROW_ID, attachmentId.getRowId())
|
||||||
|
.putLong(KEY_UNIQUE_ID, attachmentId.getUniqueId())
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
|
public @NonNull String getFactoryKey() {
|
||||||
return dataBuilder.putLong(KEY_ROW_ID, attachmentId.getRowId())
|
return KEY;
|
||||||
.putLong(KEY_UNIQUE_ID, attachmentId.getUniqueId())
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -92,7 +92,7 @@ public class AttachmentUploadJob extends ContextJob implements InjectableType {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCanceled() { }
|
public void onCanceled() { }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean onShouldRetry(Exception exception) {
|
protected boolean onShouldRetry(Exception exception) {
|
||||||
|
@ -145,4 +145,11 @@ public class AttachmentUploadJob extends ContextJob implements InjectableType {
|
||||||
throw new UndeliverableMessageException(e);
|
throw new UndeliverableMessageException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class Factory implements Job.Factory<AttachmentUploadJob> {
|
||||||
|
@Override
|
||||||
|
public @NonNull AttachmentUploadJob create(@NonNull Parameters parameters, @NonNull org.thoughtcrime.securesms.jobmanager.Data data) {
|
||||||
|
return new AttachmentUploadJob(parameters, new AttachmentId(data.getLong(KEY_ROW_ID), data.getLong(KEY_UNIQUE_ID)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
package org.thoughtcrime.securesms.jobs;
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
|
import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
|
||||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||||
|
@ -29,46 +28,41 @@ import java.io.InputStream;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.work.Data;
|
public class AvatarDownloadJob extends BaseJob implements InjectableType {
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
|
|
||||||
public class AvatarDownloadJob extends ContextJob implements InjectableType {
|
public static final String KEY = "AvatarDownloadJob";
|
||||||
|
|
||||||
private static final int MAX_AVATAR_SIZE = 20 * 1024 * 1024;
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
private static final String TAG = AvatarDownloadJob.class.getSimpleName();
|
private static final String TAG = AvatarDownloadJob.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final int MAX_AVATAR_SIZE = 20 * 1024 * 1024;
|
||||||
|
|
||||||
private static final String KEY_GROUP_ID = "group_id";
|
private static final String KEY_GROUP_ID = "group_id";
|
||||||
|
|
||||||
@Inject transient SignalServiceMessageReceiver receiver;
|
@Inject SignalServiceMessageReceiver receiver;
|
||||||
|
|
||||||
private byte[] groupId;
|
private byte[] groupId;
|
||||||
|
|
||||||
public AvatarDownloadJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
public AvatarDownloadJob(@NonNull byte[] groupId) {
|
||||||
super(context, workerParameters);
|
this(new Job.Parameters.Builder()
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setMaxAttempts(10)
|
||||||
|
.build(),
|
||||||
|
groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AvatarDownloadJob(Context context, @NonNull byte[] groupId) {
|
private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull byte[] groupId) {
|
||||||
super(context, JobParameters.newBuilder()
|
super(parameters);
|
||||||
.withNetworkRequirement()
|
|
||||||
.create());
|
|
||||||
|
|
||||||
this.groupId = groupId;
|
this.groupId = groupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize(@NonNull SafeData data) {
|
public @NonNull Data serialize() {
|
||||||
try {
|
return new Data.Builder().putString(KEY_GROUP_ID, GroupUtil.getEncodedId(groupId, false)).build();
|
||||||
groupId = GroupUtil.getDecodedId(data.getString(KEY_GROUP_ID));
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
|
public @NonNull String getFactoryKey() {
|
||||||
return dataBuilder.putString(KEY_GROUP_ID, GroupUtil.getEncodedId(groupId, false)).build();
|
return KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -122,4 +116,14 @@ public class AvatarDownloadJob extends ContextJob implements InjectableType {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class Factory implements Job.Factory<AvatarDownloadJob> {
|
||||||
|
@Override
|
||||||
|
public @NonNull AvatarDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
try {
|
||||||
|
return new AvatarDownloadJob(parameters, GroupUtil.getDecodedId(data.getString(KEY_GROUP_ID)));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.JobLogger;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
|
public abstract class BaseJob extends Job {
|
||||||
|
|
||||||
|
private static final String TAG = BaseJob.class.getSimpleName();
|
||||||
|
|
||||||
|
public BaseJob(@NonNull Parameters parameters) {
|
||||||
|
super(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull Result run() {
|
||||||
|
try {
|
||||||
|
onRun();
|
||||||
|
return Result.SUCCESS;
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (onShouldRetry(e)) {
|
||||||
|
Log.i(TAG, JobLogger.format(this, "Encountered a retryable exception."), e);
|
||||||
|
return Result.RETRY;
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, JobLogger.format(this, "Encountered a failing exception."), e);
|
||||||
|
return Result.FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void onRun() throws Exception;
|
||||||
|
|
||||||
|
protected abstract boolean onShouldRetry(@NonNull Exception e);
|
||||||
|
}
|
|
@ -1,15 +1,13 @@
|
||||||
package org.thoughtcrime.securesms.jobs;
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
|
||||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
|
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
|
||||||
import org.whispersystems.libsignal.InvalidKeyIdException;
|
import org.whispersystems.libsignal.InvalidKeyIdException;
|
||||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||||
import org.whispersystems.libsignal.state.SignedPreKeyStore;
|
import org.whispersystems.libsignal.state.SignedPreKeyStore;
|
||||||
|
@ -26,38 +24,38 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.work.Data;
|
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
|
|
||||||
import static org.thoughtcrime.securesms.dependencies.AxolotlStorageModule.SignedPreKeyStoreFactory;
|
import static org.thoughtcrime.securesms.dependencies.AxolotlStorageModule.SignedPreKeyStoreFactory;
|
||||||
|
|
||||||
public class CleanPreKeysJob extends ContextJob implements InjectableType {
|
public class CleanPreKeysJob extends BaseJob implements InjectableType {
|
||||||
|
|
||||||
|
public static final String KEY = "CleanPreKeysJob";
|
||||||
|
|
||||||
private static final String TAG = CleanPreKeysJob.class.getSimpleName();
|
private static final String TAG = CleanPreKeysJob.class.getSimpleName();
|
||||||
|
|
||||||
private static final long ARCHIVE_AGE = TimeUnit.DAYS.toMillis(7);
|
private static final long ARCHIVE_AGE = TimeUnit.DAYS.toMillis(7);
|
||||||
|
|
||||||
@Inject transient SignalServiceAccountManager accountManager;
|
@Inject SignalServiceAccountManager accountManager;
|
||||||
@Inject transient SignedPreKeyStoreFactory signedPreKeyStoreFactory;
|
@Inject SignedPreKeyStoreFactory signedPreKeyStoreFactory;
|
||||||
|
|
||||||
public CleanPreKeysJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
public CleanPreKeysJob() {
|
||||||
super(context, workerParameters);
|
this(new Job.Parameters.Builder()
|
||||||
|
.setQueue("CleanPreKeysJob")
|
||||||
|
.setMaxAttempts(5)
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public CleanPreKeysJob(Context context) {
|
private CleanPreKeysJob(@NonNull Job.Parameters parameters) {
|
||||||
super(context, JobParameters.newBuilder()
|
super(parameters);
|
||||||
.withGroupId(CleanPreKeysJob.class.getSimpleName())
|
|
||||||
.withRetryCount(5)
|
|
||||||
.create());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize(@NonNull SafeData data) {
|
public @NonNull Data serialize() {
|
||||||
|
return Data.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
|
public @NonNull String getFactoryKey() {
|
||||||
return dataBuilder.build();
|
return KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -134,4 +132,10 @@ public class CleanPreKeysJob extends ContextJob implements InjectableType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class Factory implements Job.Factory<CleanPreKeysJob> {
|
||||||
|
@Override
|
||||||
|
public @NonNull CleanPreKeysJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
return new CleanPreKeysJob(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.jobs;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent;
|
|
||||||
|
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
|
|
||||||
public abstract class ContextJob extends Job implements ContextDependent {
|
|
||||||
|
|
||||||
protected transient Context context;
|
|
||||||
|
|
||||||
public ContextJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
|
||||||
super(context, workerParameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ContextJob(@NonNull Context context, @NonNull JobParameters parameters) {
|
|
||||||
super(context, parameters);
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContext(Context context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Context getContext() {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,11 +4,11 @@ import android.content.Context;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
|
||||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
|
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||||
|
@ -20,35 +20,35 @@ import java.io.IOException;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.work.Data;
|
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
|
|
||||||
public class CreateSignedPreKeyJob extends ContextJob implements InjectableType {
|
public class CreateSignedPreKeyJob extends BaseJob implements InjectableType {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
public static final String KEY = "CreateSignedPreKeyJob";
|
||||||
|
|
||||||
private static final String TAG = CreateSignedPreKeyJob.class.getSimpleName();
|
private static final String TAG = CreateSignedPreKeyJob.class.getSimpleName();
|
||||||
|
|
||||||
@Inject transient SignalServiceAccountManager accountManager;
|
@Inject SignalServiceAccountManager accountManager;
|
||||||
|
|
||||||
public CreateSignedPreKeyJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
|
||||||
super(context, workerParameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CreateSignedPreKeyJob(Context context) {
|
public CreateSignedPreKeyJob(Context context) {
|
||||||
super(context, JobParameters.newBuilder()
|
this(new Job.Parameters.Builder()
|
||||||
.withNetworkRequirement()
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
.withGroupId(CreateSignedPreKeyJob.class.getSimpleName())
|
.setQueue("CreateSignedPreKeyJob")
|
||||||
.create());
|
.setMaxAttempts(25)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private CreateSignedPreKeyJob(@NonNull Job.Parameters parameters) {
|
||||||
|
super(parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize(@NonNull SafeData data) {
|
public @NonNull Data serialize() {
|
||||||
|
return Data.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
|
public @NonNull String getFactoryKey() {
|
||||||
return dataBuilder.build();
|
return KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -78,4 +78,11 @@ public class CreateSignedPreKeyJob extends ContextJob implements InjectableType
|
||||||
if (exception instanceof PushNetworkException) return true;
|
if (exception instanceof PushNetworkException) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class Factory implements Job.Factory<CreateSignedPreKeyJob> {
|
||||||
|
@Override
|
||||||
|
public @NonNull CreateSignedPreKeyJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
return new CreateSignedPreKeyJob(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +1,66 @@
|
||||||
package org.thoughtcrime.securesms.jobs;
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.app.Application;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.Address;
|
import org.thoughtcrime.securesms.database.Address;
|
||||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.DirectoryHelper;
|
import org.thoughtcrime.securesms.util.DirectoryHelper;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import androidx.work.Data;
|
public class DirectoryRefreshJob extends BaseJob {
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
|
|
||||||
public class DirectoryRefreshJob extends ContextJob {
|
public static final String KEY = "DirectoryRefreshJob";
|
||||||
|
|
||||||
private static final String TAG = DirectoryRefreshJob.class.getSimpleName();
|
private static final String TAG = DirectoryRefreshJob.class.getSimpleName();
|
||||||
|
|
||||||
private static final String KEY_ADDRESS = "address";
|
private static final String KEY_ADDRESS = "address";
|
||||||
private static final String KEY_NOTIFY_OF_NEW_USERS = "notify_of_new_users";
|
private static final String KEY_NOTIFY_OF_NEW_USERS = "notify_of_new_users";
|
||||||
|
|
||||||
@Nullable private transient Recipient recipient;
|
@Nullable private Recipient recipient;
|
||||||
private transient boolean notifyOfNewUsers;
|
private boolean notifyOfNewUsers;
|
||||||
|
|
||||||
public DirectoryRefreshJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
public DirectoryRefreshJob(boolean notifyOfNewUsers) {
|
||||||
super(context, workerParameters);
|
this(null, notifyOfNewUsers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DirectoryRefreshJob(@NonNull Context context, boolean notifyOfNewUsers) {
|
public DirectoryRefreshJob(@Nullable Recipient recipient,
|
||||||
this(context, null, notifyOfNewUsers);
|
boolean notifyOfNewUsers)
|
||||||
}
|
|
||||||
|
|
||||||
public DirectoryRefreshJob(@NonNull Context context,
|
|
||||||
@Nullable Recipient recipient,
|
|
||||||
boolean notifyOfNewUsers)
|
|
||||||
{
|
{
|
||||||
super(context, JobParameters.newBuilder()
|
this(new Job.Parameters.Builder()
|
||||||
.withGroupId(DirectoryRefreshJob.class.getSimpleName())
|
.setQueue("DirectoryRefreshJob")
|
||||||
.withNetworkRequirement()
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
.create());
|
.setMaxAttempts(10)
|
||||||
|
.build(),
|
||||||
|
recipient,
|
||||||
|
notifyOfNewUsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DirectoryRefreshJob(@NonNull Job.Parameters parameters, @Nullable Recipient recipient, boolean notifyOfNewUsers) {
|
||||||
|
super(parameters);
|
||||||
|
|
||||||
this.recipient = recipient;
|
this.recipient = recipient;
|
||||||
this.notifyOfNewUsers = notifyOfNewUsers;
|
this.notifyOfNewUsers = notifyOfNewUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize(@NonNull SafeData data) {
|
public @NonNull Data serialize() {
|
||||||
String serializedAddress = data.getString(KEY_ADDRESS);
|
return new Data.Builder().putString(KEY_ADDRESS, recipient != null ? recipient.getAddress().serialize() : null)
|
||||||
Address address = serializedAddress != null ? Address.fromSerialized(serializedAddress) : null;
|
.putBoolean(KEY_NOTIFY_OF_NEW_USERS, notifyOfNewUsers)
|
||||||
|
.build();
|
||||||
recipient = address != null ? Recipient.from(context, address, true) : null;
|
|
||||||
notifyOfNewUsers = data.getBoolean(KEY_NOTIFY_OF_NEW_USERS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
|
public @NonNull String getFactoryKey() {
|
||||||
return dataBuilder.putString(KEY_ADDRESS, recipient != null ? recipient.getAddress().serialize() : null)
|
return KEY;
|
||||||
.putBoolean(KEY_NOTIFY_OF_NEW_USERS, notifyOfNewUsers)
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -84,4 +82,23 @@ public class DirectoryRefreshJob extends ContextJob {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCanceled() {}
|
public void onCanceled() {}
|
||||||
|
|
||||||
|
public static final class Factory implements Job.Factory<DirectoryRefreshJob> {
|
||||||
|
|
||||||
|
private final Application application;
|
||||||
|
|
||||||
|
public Factory(@NonNull Application application) {
|
||||||
|
this.application = application;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull DirectoryRefreshJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
String serializedAddress = data.getString(KEY_ADDRESS);
|
||||||
|
Address address = serializedAddress != null ? Address.fromSerialized(serializedAddress) : null;
|
||||||
|
Recipient recipient = address != null ? Recipient.from(application, address, true) : null;
|
||||||
|
boolean notifyOfNewUsers = data.getBoolean(KEY_NOTIFY_OF_NEW_USERS);
|
||||||
|
|
||||||
|
return new DirectoryRefreshJob(parameters, recipient, notifyOfNewUsers);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,259 @@
|
||||||
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||||
|
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.util.Util;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ListIterator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class FastJobStorage implements JobStorage {
|
||||||
|
|
||||||
|
private final JobDatabase jobDatabase;
|
||||||
|
|
||||||
|
private final List<JobSpec> jobs;
|
||||||
|
private final Map<String, List<ConstraintSpec>> constraintsByJobId;
|
||||||
|
private final Map<String, List<DependencySpec>> dependenciesByJobId;
|
||||||
|
|
||||||
|
public FastJobStorage(@NonNull JobDatabase jobDatabase) {
|
||||||
|
this.jobDatabase = jobDatabase;
|
||||||
|
this.jobs = new ArrayList<>();
|
||||||
|
this.constraintsByJobId = new HashMap<>();
|
||||||
|
this.dependenciesByJobId = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void init() {
|
||||||
|
List<JobSpec> jobSpecs = jobDatabase.getAllJobSpecs();
|
||||||
|
List<ConstraintSpec> constraintSpecs = jobDatabase.getAllConstraintSpecs();
|
||||||
|
List<DependencySpec> dependencySpecs = jobDatabase.getAllDependencySpecs();
|
||||||
|
|
||||||
|
jobs.addAll(jobSpecs);
|
||||||
|
|
||||||
|
for (ConstraintSpec constraintSpec: constraintSpecs) {
|
||||||
|
List<ConstraintSpec> jobConstraints = Util.getOrDefault(constraintsByJobId, constraintSpec.getJobSpecId(), new LinkedList<>());
|
||||||
|
jobConstraints.add(constraintSpec);
|
||||||
|
constraintsByJobId.put(constraintSpec.getJobSpecId(), jobConstraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DependencySpec dependencySpec : dependencySpecs) {
|
||||||
|
List<DependencySpec> jobDependencies = Util.getOrDefault(dependenciesByJobId, dependencySpec.getJobId(), new LinkedList<>());
|
||||||
|
jobDependencies.add(dependencySpec);
|
||||||
|
dependenciesByJobId.put(dependencySpec.getJobId(), jobDependencies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void insertJobs(@NonNull List<FullSpec> fullSpecs) {
|
||||||
|
jobDatabase.insertJobs(fullSpecs);
|
||||||
|
|
||||||
|
for (FullSpec fullSpec : fullSpecs) {
|
||||||
|
jobs.add(fullSpec.getJobSpec());
|
||||||
|
constraintsByJobId.put(fullSpec.getJobSpec().getId(), fullSpec.getConstraintSpecs());
|
||||||
|
dependenciesByJobId.put(fullSpec.getJobSpec().getId(), fullSpec.getDependencySpecs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized @Nullable JobSpec getJobSpec(@NonNull String id) {
|
||||||
|
for (JobSpec jobSpec : jobs) {
|
||||||
|
if (jobSpec.getId().equals(id)) {
|
||||||
|
return jobSpec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized @NonNull List<JobSpec> getAllJobSpecs() {
|
||||||
|
return new ArrayList<>(jobs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized @NonNull List<JobSpec> getPendingJobsWithNoDependenciesInCreatedOrder(long currentTime) {
|
||||||
|
return Stream.of(jobs)
|
||||||
|
.filterNot(JobSpec::isRunning)
|
||||||
|
.filter(this::firstInQueue)
|
||||||
|
.filter(j -> !dependenciesByJobId.containsKey(j.getId()) || dependenciesByJobId.get(j.getId()).isEmpty())
|
||||||
|
.filter(j -> j.getNextRunAttemptTime() <= currentTime)
|
||||||
|
.sorted((j1, j2) -> Long.compare(j1.getCreateTime(), j2.getCreateTime()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean firstInQueue(@NonNull JobSpec job) {
|
||||||
|
if (job.getQueueKey() == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Stream.of(jobs)
|
||||||
|
.filter(j -> Util.equals(j.getQueueKey(), job.getQueueKey()))
|
||||||
|
.sorted((j1, j2) -> Long.compare(j1.getCreateTime(), j2.getCreateTime()))
|
||||||
|
.toList()
|
||||||
|
.get(0)
|
||||||
|
.equals(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int getJobInstanceCount(@NonNull String factoryKey) {
|
||||||
|
return (int) Stream.of(jobs)
|
||||||
|
.filter(j -> j.getFactoryKey().equals(factoryKey))
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void updateJobRunningState(@NonNull String id, boolean isRunning) {
|
||||||
|
jobDatabase.updateJobRunningState(id, isRunning);
|
||||||
|
|
||||||
|
ListIterator<JobSpec> iter = jobs.listIterator();
|
||||||
|
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
JobSpec existing = iter.next();
|
||||||
|
if (existing.getId().equals(id)) {
|
||||||
|
JobSpec updated = new JobSpec(existing.getId(),
|
||||||
|
existing.getFactoryKey(),
|
||||||
|
existing.getQueueKey(),
|
||||||
|
existing.getCreateTime(),
|
||||||
|
existing.getNextRunAttemptTime(),
|
||||||
|
existing.getRunAttempt(),
|
||||||
|
existing.getMaxAttempts(),
|
||||||
|
existing.getMaxBackoff(),
|
||||||
|
existing.getLifespan(),
|
||||||
|
existing.getMaxInstances(),
|
||||||
|
existing.getSerializedData(),
|
||||||
|
isRunning);
|
||||||
|
iter.set(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime) {
|
||||||
|
jobDatabase.updateJobAfterRetry(id, isRunning, runAttempt, nextRunAttemptTime);
|
||||||
|
|
||||||
|
ListIterator<JobSpec> iter = jobs.listIterator();
|
||||||
|
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
JobSpec existing = iter.next();
|
||||||
|
if (existing.getId().equals(id)) {
|
||||||
|
JobSpec updated = new JobSpec(existing.getId(),
|
||||||
|
existing.getFactoryKey(),
|
||||||
|
existing.getQueueKey(),
|
||||||
|
existing.getCreateTime(),
|
||||||
|
nextRunAttemptTime,
|
||||||
|
runAttempt,
|
||||||
|
existing.getMaxAttempts(),
|
||||||
|
existing.getMaxBackoff(),
|
||||||
|
existing.getLifespan(),
|
||||||
|
existing.getMaxInstances(),
|
||||||
|
existing.getSerializedData(),
|
||||||
|
isRunning);
|
||||||
|
iter.set(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void updateAllJobsToBePending() {
|
||||||
|
jobDatabase.updateAllJobsToBePending();
|
||||||
|
|
||||||
|
ListIterator<JobSpec> iter = jobs.listIterator();
|
||||||
|
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
JobSpec existing = iter.next();
|
||||||
|
JobSpec updated = new JobSpec(existing.getId(),
|
||||||
|
existing.getFactoryKey(),
|
||||||
|
existing.getQueueKey(),
|
||||||
|
existing.getCreateTime(),
|
||||||
|
existing.getNextRunAttemptTime(),
|
||||||
|
existing.getRunAttempt(),
|
||||||
|
existing.getMaxAttempts(),
|
||||||
|
existing.getMaxBackoff(),
|
||||||
|
existing.getLifespan(),
|
||||||
|
existing.getMaxInstances(),
|
||||||
|
existing.getSerializedData(),
|
||||||
|
false);
|
||||||
|
iter.set(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void deleteJob(@NonNull String jobId) {
|
||||||
|
deleteJobs(Collections.singletonList(jobId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void deleteJobs(@NonNull List<String> jobIds) {
|
||||||
|
jobDatabase.deleteJobs(jobIds);
|
||||||
|
|
||||||
|
Set<String> deleteIds = new HashSet<>(jobIds);
|
||||||
|
|
||||||
|
Iterator<JobSpec> jobIter = jobs.iterator();
|
||||||
|
while (jobIter.hasNext()) {
|
||||||
|
if (deleteIds.contains(jobIter.next().getId())) {
|
||||||
|
jobIter.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String jobId : jobIds) {
|
||||||
|
constraintsByJobId.remove(jobId);
|
||||||
|
dependenciesByJobId.remove(jobId);
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<DependencySpec>> entry : dependenciesByJobId.entrySet()) {
|
||||||
|
Iterator<DependencySpec> depedencyIter = entry.getValue().iterator();
|
||||||
|
|
||||||
|
while (depedencyIter.hasNext()) {
|
||||||
|
if (depedencyIter.next().getDependsOnJobId().equals(jobId)) {
|
||||||
|
depedencyIter.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized @NonNull List<ConstraintSpec> getConstraintSpecs(@NonNull String jobId) {
|
||||||
|
return Util.getOrDefault(constraintsByJobId, jobId, new LinkedList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized @NonNull List<ConstraintSpec> getAllConstraintSpecs() {
|
||||||
|
return Stream.of(constraintsByJobId)
|
||||||
|
.map(Map.Entry::getValue)
|
||||||
|
.flatMap(Stream::of)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized @NonNull List<DependencySpec> getDependencySpecsThatDependOnJob(@NonNull String jobSpecId) {
|
||||||
|
return Stream.of(dependenciesByJobId.entrySet())
|
||||||
|
.map(Map.Entry::getValue)
|
||||||
|
.flatMap(Stream::of)
|
||||||
|
.filter(j -> j.getDependsOnJobId().equals(jobSpecId))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull List<DependencySpec> getAllDependencySpecs() {
|
||||||
|
return Stream.of(dependenciesByJobId)
|
||||||
|
.map(Map.Entry::getValue)
|
||||||
|
.flatMap(Stream::of)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,13 +28,14 @@ import com.google.android.gms.common.ConnectionResult;
|
||||||
import com.google.android.gms.common.GoogleApiAvailability;
|
import com.google.android.gms.common.GoogleApiAvailability;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.gcm.FcmUtil;
|
import org.thoughtcrime.securesms.gcm.FcmUtil;
|
||||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.PlayServicesProblemActivity;
|
import org.thoughtcrime.securesms.PlayServicesProblemActivity;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
@ -43,38 +44,40 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.work.Data;
|
public class FcmRefreshJob extends BaseJob implements InjectableType {
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
|
|
||||||
public class FcmRefreshJob extends ContextJob implements InjectableType {
|
public static final String KEY = "FcmRefreshJob";
|
||||||
|
|
||||||
private static final String TAG = FcmRefreshJob.class.getSimpleName();
|
private static final String TAG = FcmRefreshJob.class.getSimpleName();
|
||||||
|
|
||||||
@Inject transient SignalServiceAccountManager textSecureAccountManager;
|
@Inject SignalServiceAccountManager textSecureAccountManager;
|
||||||
|
|
||||||
public FcmRefreshJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
public FcmRefreshJob() {
|
||||||
super(context, workerParameters);
|
this(new Job.Parameters.Builder()
|
||||||
|
.setQueue("FcmRefreshJob")
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setMaxAttempts(1)
|
||||||
|
.setLifespan(TimeUnit.MINUTES.toMillis(5))
|
||||||
|
.setMaxInstances(1)
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public FcmRefreshJob(Context context) {
|
private FcmRefreshJob(@NonNull Job.Parameters parameters) {
|
||||||
super(context, JobParameters.newBuilder()
|
super(parameters);
|
||||||
.withGroupId(FcmRefreshJob.class.getSimpleName())
|
|
||||||
.withDuplicatesIgnored(true)
|
|
||||||
.withNetworkRequirement()
|
|
||||||
.withRetryCount(1)
|
|
||||||
.create());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize(@NonNull SafeData data) {
|
public @NonNull Data serialize() {
|
||||||
|
return Data.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
|
public @NonNull String getFactoryKey() {
|
||||||
return dataBuilder.build();
|
return KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -139,4 +142,10 @@ public class FcmRefreshJob extends ContextJob implements InjectableType {
|
||||||
.notify(12, builder.build());
|
.notify(12, builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class Factory implements Job.Factory<FcmRefreshJob> {
|
||||||
|
@Override
|
||||||
|
public @NonNull FcmRefreshJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
return new FcmRefreshJob(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Constraint;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraint;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraintObserver;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class JobManagerFactories {
|
||||||
|
|
||||||
|
public static Map<String, Job.Factory> getJobFactories(@NonNull Application application) {
|
||||||
|
return new HashMap<String, Job.Factory>() {{
|
||||||
|
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());
|
||||||
|
put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory());
|
||||||
|
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
|
||||||
|
put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory());
|
||||||
|
put(CreateSignedPreKeyJob.KEY, new CreateSignedPreKeyJob.Factory());
|
||||||
|
put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory(application));
|
||||||
|
put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory());
|
||||||
|
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
||||||
|
put(MmsDownloadJob.KEY, new MmsDownloadJob.Factory());
|
||||||
|
put(MmsReceiveJob.KEY, new MmsReceiveJob.Factory());
|
||||||
|
put(MmsSendJob.KEY, new MmsSendJob.Factory());
|
||||||
|
put(MultiDeviceBlockedUpdateJob.KEY, new MultiDeviceBlockedUpdateJob.Factory());
|
||||||
|
put(MultiDeviceConfigurationUpdateJob.KEY, new MultiDeviceConfigurationUpdateJob.Factory());
|
||||||
|
put(MultiDeviceContactUpdateJob.KEY, new MultiDeviceContactUpdateJob.Factory());
|
||||||
|
put(MultiDeviceGroupUpdateJob.KEY, new MultiDeviceGroupUpdateJob.Factory());
|
||||||
|
put(MultiDeviceProfileKeyUpdateJob.KEY, new MultiDeviceProfileKeyUpdateJob.Factory());
|
||||||
|
put(MultiDeviceReadUpdateJob.KEY, new MultiDeviceReadUpdateJob.Factory());
|
||||||
|
put(MultiDeviceVerifiedUpdateJob.KEY, new MultiDeviceVerifiedUpdateJob.Factory());
|
||||||
|
put(PushContentReceiveJob.KEY, new PushContentReceiveJob.Factory());
|
||||||
|
put(PushDecryptJob.KEY, new PushDecryptJob.Factory());
|
||||||
|
put(PushGroupSendJob.KEY, new PushGroupSendJob.Factory());
|
||||||
|
put(PushGroupUpdateJob.KEY, new PushGroupUpdateJob.Factory());
|
||||||
|
put(PushMediaSendJob.KEY, new PushMediaSendJob.Factory());
|
||||||
|
put(PushNotificationReceiveJob.KEY, new PushNotificationReceiveJob.Factory());
|
||||||
|
put(PushTextSendJob.KEY, new PushTextSendJob.Factory());
|
||||||
|
put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory());
|
||||||
|
put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory());
|
||||||
|
put(RefreshUnidentifiedDeliveryAbilityJob.KEY, new RefreshUnidentifiedDeliveryAbilityJob.Factory());
|
||||||
|
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
|
||||||
|
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application));
|
||||||
|
put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory(application));
|
||||||
|
put(RotateCertificateJob.KEY, new RotateCertificateJob.Factory());
|
||||||
|
put(RotateProfileKeyJob.KEY, new RotateProfileKeyJob.Factory());
|
||||||
|
put(RotateSignedPreKeyJob.KEY, new RotateSignedPreKeyJob.Factory());
|
||||||
|
put(SendDeliveryReceiptJob.KEY, new SendDeliveryReceiptJob.Factory());
|
||||||
|
put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory());
|
||||||
|
put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory());
|
||||||
|
put(SmsReceiveJob.KEY, new SmsReceiveJob.Factory());
|
||||||
|
put(SmsSendJob.KEY, new SmsSendJob.Factory());
|
||||||
|
put(SmsSentJob.KEY, new SmsSentJob.Factory());
|
||||||
|
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
|
||||||
|
put(TypingSendJob.KEY, new TypingSendJob.Factory());
|
||||||
|
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, Constraint.Factory> getConstraintFactories(@NonNull Application application) {
|
||||||
|
return new HashMap<String, Constraint.Factory>() {{
|
||||||
|
put(CellServiceConstraint.KEY, new CellServiceConstraint.Factory(application));
|
||||||
|
put(NetworkConstraint.KEY, new NetworkConstraint.Factory(application));
|
||||||
|
put(NetworkOrCellServiceConstraint.KEY, new NetworkOrCellServiceConstraint.Factory(application));
|
||||||
|
put(SqlCipherMigrationConstraint.KEY, new SqlCipherMigrationConstraint.Factory(application));
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ConstraintObserver> getConstraintObservers(@NonNull Application application) {
|
||||||
|
return Arrays.asList(new CellServiceConstraintObserver(application),
|
||||||
|
new NetworkConstraintObserver(application),
|
||||||
|
new SqlCipherMigrationConstraintObserver());
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,11 +2,11 @@ package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.content.Context;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.backup.BackupPassphrase;
|
import org.thoughtcrime.securesms.backup.BackupPassphrase;
|
||||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
@ -14,13 +14,11 @@ import org.thoughtcrime.securesms.backup.FullBackupExporter;
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -28,31 +26,32 @@ import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import androidx.work.Data;
|
public class LocalBackupJob extends BaseJob {
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
|
|
||||||
public class LocalBackupJob extends ContextJob {
|
public static final String KEY = "LocalBackupJob";
|
||||||
|
|
||||||
private static final String TAG = LocalBackupJob.class.getSimpleName();
|
private static final String TAG = LocalBackupJob.class.getSimpleName();
|
||||||
|
|
||||||
public LocalBackupJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
public LocalBackupJob() {
|
||||||
super(context, workerParameters);
|
this(new Job.Parameters.Builder()
|
||||||
|
.setQueue("__LOCAL_BACKUP__")
|
||||||
|
.setMaxInstances(1)
|
||||||
|
.setMaxAttempts(3)
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocalBackupJob(@NonNull Context context) {
|
private LocalBackupJob(@NonNull Job.Parameters parameters) {
|
||||||
super(context, JobParameters.newBuilder()
|
super(parameters);
|
||||||
.withGroupId("__LOCAL_BACKUP__")
|
|
||||||
.withDuplicatesIgnored(true)
|
|
||||||
.create());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize(@NonNull SafeData data) {
|
public @NonNull Data serialize() {
|
||||||
|
return Data.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
|
public @NonNull String getFactoryKey() {
|
||||||
return dataBuilder.build();
|
return KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -109,6 +108,12 @@ public class LocalBackupJob extends ContextJob {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCanceled() {
|
public void onCanceled() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Factory implements Job.Factory<LocalBackupJob> {
|
||||||
|
@Override
|
||||||
|
public @NonNull LocalBackupJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
return new LocalBackupJob(parameters);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package org.thoughtcrime.securesms.jobs;
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
import com.google.android.mms.pdu_alt.CharacterSets;
|
import com.google.android.mms.pdu_alt.CharacterSets;
|
||||||
|
@ -21,7 +21,6 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
|
||||||
import org.thoughtcrime.securesms.mms.ApnUnavailableException;
|
import org.thoughtcrime.securesms.mms.ApnUnavailableException;
|
||||||
import org.thoughtcrime.securesms.mms.CompatMmsConnection;
|
import org.thoughtcrime.securesms.mms.CompatMmsConnection;
|
||||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||||
|
@ -46,12 +45,9 @@ import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import androidx.work.Data;
|
public class MmsDownloadJob extends BaseJob {
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
|
|
||||||
public class MmsDownloadJob extends ContextJob {
|
public static final String KEY = "MmsDownloadJob";
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
private static final String TAG = MmsDownloadJob.class.getSimpleName();
|
private static final String TAG = MmsDownloadJob.class.getSimpleName();
|
||||||
|
|
||||||
|
@ -63,14 +59,19 @@ public class MmsDownloadJob extends ContextJob {
|
||||||
private long threadId;
|
private long threadId;
|
||||||
private boolean automatic;
|
private boolean automatic;
|
||||||
|
|
||||||
public MmsDownloadJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
public MmsDownloadJob(long messageId, long threadId, boolean automatic) {
|
||||||
super(context, workerParameters);
|
this(new Job.Parameters.Builder()
|
||||||
|
.setQueue("mms-operation")
|
||||||
|
.setMaxAttempts(25)
|
||||||
|
.build(),
|
||||||
|
messageId,
|
||||||
|
threadId,
|
||||||
|
automatic);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public MmsDownloadJob(Context context, long messageId, long threadId, boolean automatic) {
|
private MmsDownloadJob(@NonNull Job.Parameters parameters, long messageId, long threadId, boolean automatic) {
|
||||||
super(context, JobParameters.newBuilder()
|
super(parameters);
|
||||||
.withGroupId("mms-operation")
|
|
||||||
.create());
|
|
||||||
|
|
||||||
this.messageId = messageId;
|
this.messageId = messageId;
|
||||||
this.threadId = threadId;
|
this.threadId = threadId;
|
||||||
|
@ -78,18 +79,16 @@ public class MmsDownloadJob extends ContextJob {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize(@NonNull SafeData data) {
|
public @NonNull Data serialize() {
|
||||||
messageId = data.getLong(KEY_MESSAGE_ID);
|
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
|
||||||
threadId = data.getLong(KEY_THREAD_ID);
|
.putLong(KEY_THREAD_ID, threadId)
|
||||||
automatic = data.getBoolean(KEY_AUTOMATIC);
|
.putBoolean(KEY_AUTOMATIC, automatic)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
|
public @NonNull String getFactoryKey() {
|
||||||
return dataBuilder.putLong(KEY_MESSAGE_ID, messageId)
|
return KEY;
|
||||||
.putLong(KEY_THREAD_ID, threadId)
|
|
||||||
.putBoolean(KEY_AUTOMATIC, automatic)
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -269,4 +268,14 @@ public class MmsDownloadJob extends ContextJob {
|
||||||
MessageNotifier.updateNotification(context, threadId);
|
MessageNotifier.updateNotification(context, threadId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class Factory implements Job.Factory<MmsDownloadJob> {
|
||||||
|
@Override
|
||||||
|
public @NonNull MmsDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
return new MmsDownloadJob(parameters,
|
||||||
|
data.getLong(KEY_MESSAGE_ID),
|
||||||
|
data.getLong(KEY_THREAD_ID),
|
||||||
|
data.getBoolean(KEY_AUTOMATIC));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
package org.thoughtcrime.securesms.jobs;
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
import android.content.Context;
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
@ -17,19 +16,15 @@ import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.database.Address;
|
import org.thoughtcrime.securesms.database.Address;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.Base64;
|
import org.thoughtcrime.securesms.util.Base64;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import androidx.work.Data;
|
public class MmsReceiveJob extends BaseJob {
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
|
|
||||||
public class MmsReceiveJob extends ContextJob {
|
public static final String KEY = "MmsReceiveJob";
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
private static final String TAG = MmsReceiveJob.class.getSimpleName();
|
private static final String TAG = MmsReceiveJob.class.getSimpleName();
|
||||||
|
|
||||||
|
@ -39,32 +34,27 @@ public class MmsReceiveJob extends ContextJob {
|
||||||
private byte[] data;
|
private byte[] data;
|
||||||
private int subscriptionId;
|
private int subscriptionId;
|
||||||
|
|
||||||
public MmsReceiveJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
public MmsReceiveJob(byte[] data, int subscriptionId) {
|
||||||
super(context, workerParameters);
|
this(new Job.Parameters.Builder().setMaxAttempts(25).build(), data, subscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MmsReceiveJob(Context context, byte[] data, int subscriptionId) {
|
private MmsReceiveJob(@NonNull Job.Parameters parameters, byte[] data, int subscriptionId) {
|
||||||
super(context, JobParameters.newBuilder().create());
|
super(parameters);
|
||||||
|
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.subscriptionId = subscriptionId;
|
this.subscriptionId = subscriptionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize(@NonNull SafeData data) {
|
public @NonNull Data serialize() {
|
||||||
try {
|
return new Data.Builder().putString(KEY_DATA, Base64.encodeBytes(data))
|
||||||
this.data = Base64.decode(data.getString(KEY_DATA));
|
.putInt(KEY_SUBSCRIPTION_ID, subscriptionId)
|
||||||
} catch (IOException e) {
|
.build();
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
subscriptionId = data.getInt(KEY_SUBSCRIPTION_ID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
|
public @NonNull String getFactoryKey() {
|
||||||
return dataBuilder.putString(KEY_DATA, Base64.encodeBytes(data))
|
return KEY;
|
||||||
.putInt(KEY_SUBSCRIPTION_ID, subscriptionId)
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -91,8 +81,7 @@ public class MmsReceiveJob extends ContextJob {
|
||||||
|
|
||||||
ApplicationContext.getInstance(context)
|
ApplicationContext.getInstance(context)
|
||||||
.getJobManager()
|
.getJobManager()
|
||||||
.add(new MmsDownloadJob(context,
|
.add(new MmsDownloadJob(messageAndThreadId.first,
|
||||||
messageAndThreadId.first,
|
|
||||||
messageAndThreadId.second,
|
messageAndThreadId.second,
|
||||||
true));
|
true));
|
||||||
} else if (isNotification(pdu)) {
|
} else if (isNotification(pdu)) {
|
||||||
|
@ -122,4 +111,15 @@ public class MmsReceiveJob extends ContextJob {
|
||||||
private boolean isNotification(GenericPdu pdu) {
|
private boolean isNotification(GenericPdu pdu) {
|
||||||
return pdu != null && pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
|
return pdu != null && pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class Factory implements Job.Factory<MmsReceiveJob> {
|
||||||
|
@Override
|
||||||
|
public @NonNull MmsReceiveJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
try {
|
||||||
|
return new MmsReceiveJob(parameters, Base64.decode(data.getString(KEY_DATA)), data.getInt(KEY_SUBSCRIPTION_ID));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,9 @@ import android.content.Context;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
@ -28,7 +30,6 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
|
||||||
import org.thoughtcrime.securesms.mms.CompatMmsConnection;
|
import org.thoughtcrime.securesms.mms.CompatMmsConnection;
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||||
import org.thoughtcrime.securesms.mms.MmsException;
|
import org.thoughtcrime.securesms.mms.MmsException;
|
||||||
|
@ -49,12 +50,9 @@ import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import androidx.work.Data;
|
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
|
|
||||||
public class MmsSendJob extends SendJob {
|
public class MmsSendJob extends SendJob {
|
||||||
|
|
||||||
private static final long serialVersionUID = 0L;
|
public static final String KEY = "MmsSendJob";
|
||||||
|
|
||||||
private static final String TAG = MmsSendJob.class.getSimpleName();
|
private static final String TAG = MmsSendJob.class.getSimpleName();
|
||||||
|
|
||||||
|
@ -62,28 +60,28 @@ public class MmsSendJob extends SendJob {
|
||||||
|
|
||||||
private long messageId;
|
private long messageId;
|
||||||
|
|
||||||
public MmsSendJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
public MmsSendJob(long messageId) {
|
||||||
super(context, workerParameters);
|
this(new Job.Parameters.Builder()
|
||||||
|
.setQueue("mms-operation")
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setMaxAttempts(15)
|
||||||
|
.build(),
|
||||||
|
messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MmsSendJob(Context context, long messageId) {
|
private MmsSendJob(@NonNull Job.Parameters parameters, long messageId) {
|
||||||
super(context, JobParameters.newBuilder()
|
super(parameters);
|
||||||
.withGroupId("mms-operation")
|
|
||||||
.withNetworkRequirement()
|
|
||||||
.withRetryCount(15)
|
|
||||||
.create());
|
|
||||||
|
|
||||||
this.messageId = messageId;
|
this.messageId = messageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize(@NonNull SafeData data) {
|
public @NonNull Data serialize() {
|
||||||
messageId = data.getLong(KEY_MESSAGE_ID);
|
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
|
public @NonNull String getFactoryKey() {
|
||||||
return dataBuilder.putLong(KEY_MESSAGE_ID, messageId).build();
|
return KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -318,4 +316,11 @@ public class MmsSendJob extends SendJob {
|
||||||
throw new UndeliverableMessageException(e);
|
throw new UndeliverableMessageException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Factory implements Job.Factory<MmsSendJob> {
|
||||||
|
@Override
|
||||||
|
public @NonNull MmsSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
return new MmsSendJob(parameters, data.getLong(KEY_MESSAGE_ID));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue