Migrated to new JobManager.

master
Greyson Parrelli 2019-03-28 08:56:35 -07:00
parent 8cf3ba424a
commit 4a3c173adb
162 changed files with 5360 additions and 3574 deletions

View File

@ -659,6 +659,29 @@
</intent-filter>
</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"/>
<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" />

View File

@ -70,7 +70,6 @@ dependencies {
compile 'com.android.support:multidex:1.0.3'
compile 'android.arch.lifecycle:extensions: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') {
exclude group: 'com.google.firebase', module: 'firebase-core'
@ -177,7 +176,6 @@ dependencyVerification {
'com.android.support:exifinterface:bbf44e519edd6333a24a3285aa21fd00181b920b81ca8aa89a8899f03ab4d6b0',
'com.android.support.constraint:constraint-layout:27b4e5c0b80d3ff8b92f4c93b3b4d3ecf16c01589f4cdf70ca7cf64cb42d8122',
'com.android.support:multidex:ecf6098572e23b5155bab3b9a82b2fd1530eda6c6c157745e0f5287c66eec60c',
'android.arch.work:work-runtime:51a3c9c85bef74d60737b40bb5ab5d1f5a2b825394d26e15f19b7316ac2bff20',
'android.arch.lifecycle:extensions:429426b2feec2245ffc5e75b3b5309bedb36159cf06dc71843ae43526ac289b6',
'android.arch.lifecycle:common-java8:7078b5c8ccb94203df9cc2a463c69cf0021596e6cf966d78fbfd697aaafe0630',
'com.google.firebase:firebase-messaging:e42288e7950d7d3b033d3395a5ac9365d230da3e439a2794ec13e2ef0fbaf078',
@ -223,7 +221,6 @@ dependencyVerification {
'com.android.support:support-fragment:3772fc738ada86824ba1a4b3f197c3dbd67b7ddcfe2c9db1de95ef2e3487a915',
'com.android.support:animated-vector-drawable:271ecbc906cda8dcd9e655ba0473129c3408a4189c806f616c378e6fd18fb3b7',
'com.android.support:support-core-ui:bbc7f65fc95649464733af373361532ab5f9f3b749c3badaa2bbf27e574b6c6f',
'android.arch.persistence.room:runtime:d05c78d494dc700fd6dbc0e873451aebb2510ffbb070c82179055cb10bdd8822',
'com.android.support:support-core-utils:c81e1e98ca3cb2edae002c69cf35b22aec364b8cb2f1042c97e206eb5790ac41',
'com.android.support:support-vector-drawable:f658986d968172bccfed28578471c96050780fe5e133861e4d331069cc373f4d',
'com.android.support:transition:45d09fc51284c17bbab300f5122512ac7d7348a6d23bda2051648bbe76cc9aa5',
@ -252,12 +249,8 @@ dependencyVerification {
'com.android.support:localbroadcastmanager:d287c823af5fdde72c099fcfc5f630efe9687af7a914343ae6fd92de32c8a806',
'com.android.support:print:4be8a812d73e4a80e35b91ceae127def3f0bb9726bf3bc439aa0cc81503f5728',
'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.constraint:constraint-layout-solver:2cafbe356f71c208013d021f32943904798cd6459e5107f9fe27000eb5bc2aef',
'com.google.guava:listenablefuture:e4ad7607e5c0477c6f890ef26a49cb8d1bb4dffb650bab4502afee64644e3069',
'org.signal:signal-metadata-android:d9d798aab7ee7200373ecff8718baf8aaeb632c123604e8a41b7b4c0c97eeee1',
'org.whispersystems:signal-service-java:fde1a008fe42ebbf1cd35018b363135cd8fec9e690304f8917b5ffb7080fa2a5',
'com.github.bumptech.glide:disklrucache:c1b1b6f5bbd01e2fcdc9d7f60913c8d338bdb65ed4a93bfa02b56f19daaade4b',
@ -373,7 +366,6 @@ android {
'proguard-retrolambda.pro',
'proguard-okhttp.pro',
'proguard-ez-vcard.pro',
'proguard-workmanager.pro',
'proguard.cfg'
testProguardFiles 'proguard-automation.pro',
'proguard.cfg'
@ -422,6 +414,7 @@ android {
}
test {
java.srcDirs = ['test/unitTest/java']
resources.srcDirs = ['test/unitTest/resources']
}
website.manifest.srcFile 'website/AndroidManifest.xml'

View File

@ -1,2 +0,0 @@
-dontwarn sun.misc.Unsafe
-dontwarn com.google.common.util.concurrent.ListenableFuture

View File

@ -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>

View File

@ -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>

View File

@ -32,12 +32,14 @@ import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.crypto.PRNGFixes;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule;
import org.thoughtcrime.securesms.jobmanager.DependencyInjector;
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.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
@ -71,8 +73,6 @@ import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import androidx.work.Configuration;
import androidx.work.WorkManager;
import dagger.ObjectGraph;
/**
@ -90,7 +90,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
private ExpiringMessageManager expiringMessageManager;
private TypingStatusRepository typingStatusRepository;
private TypingStatusSender typingStatusSender;
private JobManager jobManager;
private JobManager jobManager;
private IncomingMessageObserver incomingMessageObserver;
private ObjectGraph objectGraph;
private PersistentLogger persistentLogger;
@ -189,11 +189,14 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
}
private void initializeJobManager() {
WorkManager.initialize(this, new Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.INFO)
.build());
this.jobManager = new JobManager(this, WorkManager.getInstance());
this.jobManager = new JobManager(this, new JobManager.Configuration.Builder()
.setDataSerializer(new JsonDataSerializer())
.setJobFactories(JobManagerFactories.getJobFactories(this))
.setConstraintFactories(JobManagerFactories.getConstraintFactories(this))
.setConstraintObservers(JobManagerFactories.getConstraintObservers(this))
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(this)))
.setDependencyInjector(this)
.build());
}
public void initializeMessageRetrieval() {
@ -210,7 +213,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
long nextSetTime = TextSecurePreferences.getFcmTokenLastSetTime(this) + TimeUnit.HOURS.toMillis(6);
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() {
if (TextSecurePreferences.isMultiDevice(this) && !TextSecurePreferences.isUnidentifiedDeliveryEnabled(this)) {
jobManager.add(new RefreshUnidentifiedDeliveryAbilityJob(this));
jobManager.add(new RefreshUnidentifiedDeliveryAbilityJob());
}
}

View File

@ -27,8 +27,6 @@ import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
@ -202,7 +200,7 @@ public class ConversationListFragment extends Fragment
} else if (ExpiredBuildReminder.isEligible()) {
return Optional.of(new ExpiredBuildReminder(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));
} else if (OutdatedBuildReminder.isEligible()) {
return Optional.of(new OutdatedBuildReminder(context));

View File

@ -381,7 +381,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
return false;
}
ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceProfileKeyUpdateJob(context));
ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceProfileKeyUpdateJob());
return true;
}

View File

@ -26,10 +26,6 @@ import android.os.AsyncTask;
import android.os.Bundle;
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.contacts.avatars.ContactColorsLegacy;
import org.thoughtcrime.securesms.logging.Log;
@ -261,7 +257,7 @@ public class DatabaseUpgradeActivity extends BaseActivity {
if (params[0] < CONTACTS_ACCOUNT_VERSION) {
ApplicationContext.getInstance(getApplicationContext())
.getJobManager()
.add(new DirectoryRefreshJob(getApplicationContext(), false));
.add(new DirectoryRefreshJob(false));
}
if (params[0] < MEDIA_DOWNLOAD_CONTROLS_VERSION) {
@ -271,16 +267,16 @@ public class DatabaseUpgradeActivity extends BaseActivity {
if (params[0] < REDPHONE_SUPPORT_VERSION) {
ApplicationContext.getInstance(getApplicationContext())
.getJobManager()
.add(new RefreshAttributesJob(getApplicationContext()));
.add(new RefreshAttributesJob());
ApplicationContext.getInstance(getApplicationContext())
.getJobManager()
.add(new DirectoryRefreshJob(getApplicationContext(), false));
.add(new DirectoryRefreshJob(false));
}
if (params[0] < PROFILES) {
ApplicationContext.getInstance(getApplicationContext())
.getJobManager()
.add(new DirectoryRefreshJob(getApplicationContext(), false));
.add(new DirectoryRefreshJob(false));
}
if (params[0] < SCREENSHOTS) {
@ -335,17 +331,18 @@ public class DatabaseUpgradeActivity extends BaseActivity {
}
}
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());
for (Job job : storage.getAllUnencrypted()) {
jobManager.add(job);
Log.i(TAG, "Migrated job with class '" + job.getClass().getSimpleName() + "' to run on new JobManager.");
}
}
// This migration became unnecessary after switching away from 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());
//
// for (Job job : storage.getAllUnencrypted()) {
// jobManager.add(job);
// Log.i(TAG, "Migrated job with class '" + job.getClass().getSimpleName() + "' to run on new JobManager.");
// }
// }
if (params[0] < COLOR_MIGRATION) {
long startTime = System.currentTimeMillis();
@ -372,14 +369,14 @@ public class DatabaseUpgradeActivity extends BaseActivity {
Log.i(TAG, "Scheduling UD attributes refresh.");
ApplicationContext.getInstance(context)
.getJobManager()
.add(new RefreshAttributesJob(context));
.add(new RefreshAttributesJob());
}
if (params[0] < SIGNALING_KEY_DEPRECATION) {
Log.i(TAG, "Scheduling a RefreshAttributesJob to remove the signaling key remotely.");
ApplicationContext.getInstance(context)
.getJobManager()
.add(new RefreshAttributesJob(context));
.add(new RefreshAttributesJob());
}
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() + ".");
ApplicationContext.getInstance(context)
.getJobManager()
.add(new AttachmentDownloadJob(context, attachment.getMmsId(), attachment.getAttachmentId(), false));
.add(new AttachmentDownloadJob(attachment.getMmsId(), attachment.getAttachmentId(), false));
}
reader.close();
}

View File

@ -177,7 +177,7 @@ public class DeviceListFragment extends ListFragment
ApplicationContext.getInstance(getContext())
.getJobManager()
.add(new RefreshUnidentifiedDeliveryAbilityJob(getContext()));
.add(new RefreshUnidentifiedDeliveryAbilityJob());
} catch (IOException e) {
Log.w(TAG, e);
Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();

View File

@ -8,7 +8,6 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.thoughtcrime.securesms.components.TypingIndicatorView;
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -48,8 +47,7 @@ public class LinkPreviewsIntroFragment extends Fragment {
view.findViewById(R.id.experience_ok_button).setOnClickListener(v -> {
ApplicationContext.getInstance(requireContext())
.getJobManager()
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
.add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(requireContext()),
TextSecurePreferences.isLinkPreviewsEnabled(requireContext())));

View File

@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.registration.WelcomeActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import java.util.Locale;

View File

@ -38,8 +38,7 @@ public class ReadReceiptsIntroFragment extends Fragment {
TextSecurePreferences.setReadReceiptsEnabled(getContext(), isChecked);
ApplicationContext.getInstance(getContext())
.getJobManager()
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
isChecked,
.add(new MultiDeviceConfigurationUpdateJob(isChecked,
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));

View File

@ -771,12 +771,12 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
if (blocked && (recipient.resolve().isSystemContact() || recipient.resolve().isProfileSharing())) {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new RotateProfileKeyJob(context));
.add(new RotateProfileKeyJob());
}
ApplicationContext.getInstance(context)
.getJobManager()
.add(new MultiDeviceBlockedUpdateJob(context));
.add(new MultiDeviceBlockedUpdateJob());
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

View File

@ -91,7 +91,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
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.internal.push.LockedException;
@ -729,7 +728,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
}
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));
DirectoryRefreshListener.schedule(RegistrationActivity.this);

View File

@ -59,8 +59,7 @@ public class TypingIndicatorIntroFragment extends Fragment {
TextSecurePreferences.setTypingIndicatorsEnabled(getContext(), typingEnabled);
ApplicationContext.getInstance(requireContext())
.getJobManager()
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
.add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
typingEnabled,
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));

View File

@ -603,8 +603,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
ApplicationContext.getInstance(getActivity())
.getJobManager()
.add(new MultiDeviceVerifiedUpdateJob(getActivity(),
recipient.getAddress(),
.add(new MultiDeviceVerifiedUpdateJob(recipient.getAddress(),
remoteIdentity,
isChecked ? VerifiedStatus.VERIFIED :
VerifiedStatus.DEFAULT));

View File

@ -6,8 +6,6 @@ import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.ApplicationContext;
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 java.util.HashMap;
@ -76,7 +74,7 @@ public class TypingStatusSender {
}
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 {

View File

@ -36,10 +36,7 @@ import org.thoughtcrime.securesms.logging.Log;
import android.view.OrientationEventListener;
import android.view.ViewGroup;
import org.thoughtcrime.securesms.ApplicationContext;
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.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;

View File

@ -253,7 +253,7 @@ public class SharedContactDetailsActivity extends PassphraseRequiredActionBarAct
if (requestCode == CODE_ADD_EDIT_CONTACT && contact != null) {
ApplicationContext.getInstance(getApplicationContext())
.getJobManager()
.add(new DirectoryRefreshJob(getApplicationContext(), false));
.add(new DirectoryRefreshJob(false));
}
}
}

View File

@ -852,7 +852,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
ApplicationContext.getInstance(ConversationActivity.this)
.getJobManager()
.add(new MultiDeviceBlockedUpdateJob(ConversationActivity.this));
.add(new MultiDeviceBlockedUpdateJob());
return null;
}
@ -1377,7 +1377,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} else if (ExpiredBuildReminder.isEligible()) {
reminderView.get().showReminder(new ExpiredBuildReminder(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));
} else if (TextSecurePreferences.isPushRegistered(this) &&
TextSecurePreferences.isShowInviteReminders(this) &&
@ -1629,7 +1629,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
ApplicationContext.getInstance(this)
.getJobManager()
.add(new RetrieveProfileJob(this, recipient));
.add(new RetrieveProfileJob(recipient));
}
@Override

View File

@ -992,7 +992,7 @@ public class ConversationFragment extends Fragment
if (requestCode == CODE_ADD_EDIT_CONTACT && getContext() != null) {
ApplicationContext.getInstance(getContext().getApplicationContext())
.getJobManager()
.add(new DirectoryRefreshJob(getContext().getApplicationContext(), false));
.add(new DirectoryRefreshJob(false));
}
}

View File

@ -1043,7 +1043,7 @@ public class ConversationItem extends LinearLayout
Log.i(TAG, "Scheduling MMS attachment download");
ApplicationContext.getInstance(context)
.getJobManager()
.add(new MmsDownloadJob(context, messageRecord.getId(),
.add(new MmsDownloadJob(messageRecord.getId(),
messageRecord.getThreadId(), false));
} else {
Log.i(TAG, "Scheduling push attachment downloads for " + slides.size() + " items");
@ -1051,7 +1051,7 @@ public class ConversationItem extends LinearLayout
for (Slide slide : slides) {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new AttachmentDownloadJob(context, messageRecord.getId(),
.add(new AttachmentDownloadJob(messageRecord.getId(),
((DatabaseAttachment)slide.asAttachment()).getAttachmentId(), true));
}
}
@ -1171,7 +1171,7 @@ public class ConversationItem extends LinearLayout
ApplicationContext.getInstance(context)
.getJobManager()
.add(new MmsSendJob(context, messageRecord.getId()));
.add(new MmsSendJob(messageRecord.getId()));
} else {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
database.markAsInsecure(messageRecord.getId());

View File

@ -57,6 +57,7 @@ public class DatabaseFactory {
private final SignedPreKeyDatabase signedPreKeyDatabase;
private final SessionDatabase sessionDatabase;
private final SearchDatabase searchDatabase;
private final JobDatabase jobDatabase;
public static DatabaseFactory getInstance(Context context) {
synchronized (lock) {
@ -135,6 +136,10 @@ public class DatabaseFactory {
return getInstance(context).searchDatabase;
}
public static JobDatabase getJobDatabase(Context context) {
return getInstance(context).jobDatabase;
}
public static SQLiteDatabase getBackupDatabase(Context context) {
return getInstance(context).databaseHelper.getReadableDatabase();
}
@ -168,6 +173,7 @@ public class DatabaseFactory {
this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper);
this.sessionDatabase = new SessionDatabase(context, databaseHelper);
this.searchDatabase = new SearchDatabase(context, databaseHelper);
this.jobDatabase = new JobDatabase(context, databaseHelper);
}
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,

View File

@ -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)));
}
}

View File

@ -50,7 +50,6 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.logging.Log;
@ -176,11 +175,8 @@ public class MmsDatabase extends MessagingDatabase {
private final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache();
private final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache();
private final JobManager jobManager;
public MmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
this.jobManager = ApplicationContext.getInstance(context).getJobManager();
}
@Override
@ -837,7 +833,7 @@ public class MmsDatabase extends MessagingDatabase {
}
notifyConversationListeners(threadId);
jobManager.add(new TrimThreadJob(context, threadId));
ApplicationContext.getInstance(context).getJobManager().add(new TrimThreadJob(threadId));
return Optional.of(new InsertResult(messageId, threadId));
}
@ -918,7 +914,7 @@ public class MmsDatabase extends MessagingDatabase {
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,
@ -983,7 +979,7 @@ public class MmsDatabase extends MessagingDatabase {
DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId);
DatabaseFactory.getThreadDatabase(context).setHasSent(threadId, true);
jobManager.add(new TrimThreadJob(context, threadId));
ApplicationContext.getInstance(context).getJobManager().add(new TrimThreadJob(threadId));
return messageId;
}

View File

@ -35,7 +35,6 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
import org.thoughtcrime.securesms.logging.Log;
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 earlyReadReceiptCache = new EarlyReceiptCache();
private final JobManager jobManager;
public SmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
this.jobManager = ApplicationContext.getInstance(context).getJobManager();
}
protected String getTableName() {
@ -473,7 +469,7 @@ public class SmsDatabase extends MessagingDatabase {
DatabaseFactory.getThreadDatabase(context).update(record.getThreadId(), true);
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());
} catch (NoSuchMessageException e) {
@ -511,7 +507,7 @@ public class SmsDatabase extends MessagingDatabase {
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
notifyConversationListeners(threadId);
jobManager.add(new TrimThreadJob(context, threadId));
ApplicationContext.getInstance(context).getJobManager().add(new TrimThreadJob(threadId));
if (unread) {
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
@ -604,7 +600,7 @@ public class SmsDatabase extends MessagingDatabase {
notifyConversationListeners(threadId);
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));
@ -662,7 +658,7 @@ public class SmsDatabase extends MessagingDatabase {
notifyConversationListeners(threadId);
if (!message.isIdentityVerified() && !message.isIdentityDefault()) {
jobManager.add(new TrimThreadJob(context, threadId));
ApplicationContext.getInstance(context).getJobManager().add(new TrimThreadJob(threadId));
}
return messageId;

View File

@ -10,6 +10,7 @@ import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.JobDatabase;
import org.thoughtcrime.securesms.logging.Log;
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 SELF_ATTACHMENT_CLEANUP = 18;
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 final Context context;
@ -107,6 +109,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
for (String sql : SearchDatabase.CREATE_TABLE) {
db.execSQL(sql);
}
for (String sql : JobDatabase.CREATE_TABLE) {
db.execSQL(sql);
}
executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS);
@ -128,7 +133,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
else TextSecurePreferences.setNeedsSqlCipherMigration(context, true);
if (!PreKeyMigrationHelper.migratePreKeys(context, db)) {
ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob(context));
ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob());
}
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)");
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");
}
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();
} finally {
db.endTransaction();

View File

@ -25,7 +25,6 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceReadReceiptUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
@ -88,7 +87,6 @@ import dagger.Provides;
RetrieveProfileAvatarJob.class,
MultiDeviceProfileKeyUpdateJob.class,
SendReadReceiptJob.class,
MultiDeviceReadReceiptUpdateJob.class,
AppProtectionPreferenceFragment.class,
FcmService.class,
RotateCertificateJob.class,

View File

@ -10,7 +10,7 @@ import com.google.firebase.messaging.RemoteMessage;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.R;
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.PushNotificationReceiveJob;
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.WakeLockUtil;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.internal.util.Util;
@ -64,7 +63,7 @@ public class FcmService extends FirebaseMessagingService implements InjectableTy
ApplicationContext.getInstance(getApplicationContext())
.getJobManager()
.add(new FcmRefreshJob(getApplicationContext()));
.add(new FcmRefreshJob());
}
private void handleReceivedNotification(Context context) {
@ -78,7 +77,7 @@ public class FcmService extends FirebaseMessagingService implements InjectableTy
long startTime = System.currentTimeMillis();
PowerManager powerManager = ServiceUtil.getPowerManager(getApplicationContext());
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 AtomicBoolean foregroundRunning = new AtomicBoolean(false);

View File

@ -166,7 +166,7 @@ public class GroupMessageProcessor {
if (record.getMembers().contains(Address.fromExternal(context, content.getSender()))) {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new PushGroupUpdateJob(context, content.getSender(), group.getGroupId()));
.add(new PushGroupUpdateJob(content.getSender(), group.getGroupId()));
}
return null;
@ -204,7 +204,7 @@ public class GroupMessageProcessor {
{
if (group.getAvatar().isPresent()) {
ApplicationContext.getInstance(context).getJobManager()
.add(new AvatarDownloadJob(context, group.getGroupId()));
.add(new AvatarDownloadJob(group.getGroupId()));
}
try {

View File

@ -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();
}
}
}

View File

@ -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.");
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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.");
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -14,11 +14,11 @@
* 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;
package org.thoughtcrime.securesms.jobmanager;
/**
* Interface responsible for injecting dependencies into Jobs.
*/
public interface DependencyInjector {
public void injectDependencies(Object object);
void injectDependencies(Object object);
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -1,244 +1,281 @@
package org.thoughtcrime.securesms.jobmanager;
import android.annotation.SuppressLint;
import android.content.Context;
import android.support.annotation.NonNull;
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.service.GenericForegroundService;
import java.io.Serializable;
import java.util.Collections;
import java.util.UUID;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import androidx.work.Data;
import androidx.work.ListenableWorker.Result;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
/**
* A durable unit of work.
*
* Jobs have {@link Parameters} that describe the conditions upon when you'd like them to run, how
* often they should be retried, and how long they should be retried for.
*
* Never rely on a specific instance of this class being run. It can be created and destroyed as the
* job is retried. State that you want to save is persisted to a {@link Data} object in
* {@link #serialize()}. Your job is then recreated using a {@link Factory} that you register in
* {@link JobManager.Configuration.Builder#setJobFactories(Map)}, which is given the saved
* {@link Data} bundle.
*/
public abstract class Job {
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";
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));
public Job(@NonNull Parameters parameters) {
this.parameters = parameters;
}
@Override
public @NonNull Result doWork() {
log("doWork()" + logSuffix());
try (WorkLockManager.WorkLock workLock = WORK_LOCK_MANAGER.acquire(getId())) {
Result result = workLock.getResult();
if (result == null) {
result = doWorkInternal();
workLock.setResult(result);
} else {
log("Using result from preempted run (" + result + ")." + logSuffix());
}
return result;
}
public final String getId() {
return id;
}
private @NonNull Result doWorkInternal() {
Data data = getInputData();
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();
}
public final @NonNull Parameters getParameters() {
return parameters;
}
@Override
public void onStopped() {
log("onStopped()" + logSuffix());
public final int getRunAttempt() {
return runAttempt;
}
final void onSubmit(@NonNull Context context, @NonNull UUID id) {
Log.i(TAG, buildLog(id, "onSubmit() network: " + (new NetworkRequirement(getApplicationContext()).isPresent())));
public final long getNextRunAttemptTime() {
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();
}
/**
* Called after a run has finished and we've determined a retry is required, but before the next
* attempt is run.
* Called when the job is first submitted to the {@link JobManager}.
*/
protected void onRetry() { }
/**
* Called after a job has been added to the JobManager queue. Invoked off the main thread, so its
* safe to do longer-running work. However, work should finish relatively quickly, as it will
* block the submission of future tasks.
*/
protected void onAdded() { }
/**
* All instance state needs to be persisted in the provided {@link Data.Builder} so that it can
* be restored in {@link #initialize(SafeData)}.
* @param dataBuilder The builder where you put your state.
* @return The result of {@code dataBuilder.build()}.
*/
protected abstract @NonNull Data serialize(@NonNull Data.Builder dataBuilder);
/**
* Restore all of your instance state from the provided {@link Data}. It should contain all of
* the data put in during {@link #serialize(Data.Builder)}.
* @param data Where your data is stored.
*/
protected abstract void initialize(@NonNull SafeData data);
/**
* Called to actually execute the job.
* @throws Exception
*/
public abstract void onRun() throws Exception;
/**
* Called if a job fails to run (onShouldRetry returned false, or the number of retries exceeded
* the job's configured retry count.
*/
protected abstract void onCanceled();
/**
* If onRun() throws an exception, this method will be called to determine whether the
* job should be retried.
*
* @param exception The exception onRun() threw.
* @return true if onRun() should be called again, false otherwise.
*/
protected abstract boolean onShouldRetry(Exception exception);
@Nullable JobParameters getJobParameters() {
return parameters;
@WorkerThread
public void onAdded() {
}
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();
return Result.retry();
/**
* Serialize your job state so that it can be recreated in the future.
*/
public abstract @NonNull Data serialize();
/**
* Returns the key that can be used to find the relevant factory needed to create your job.
*/
public abstract @NonNull String getFactoryKey();
/**
* Called to do your actual work.
*/
@WorkerThread
public abstract @NonNull Result run();
/**
* Called when your job has completely failed.
*/
@WorkerThread
public abstract void onCanceled();
public interface Factory<T extends Job> {
@NonNull T create(@NonNull Parameters parameters, @NonNull Data data);
}
private Result cancel() {
onCanceled();
return Result.success(new Data.Builder().putBoolean(KEY_FAILED, true).build());
public enum Result {
SUCCESS, FAILURE, RETRY
}
private boolean requirementsMet(@NonNull Data data) {
boolean met = true;
public static final class Parameters {
if (data.getBoolean(KEY_REQUIRES_SQLCIPHER, false)) {
met &= new SqlCipherMigrationRequirement(getApplicationContext()).isPresent();
public static final int IMMORTAL = -1;
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;
}
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;
public long getCreateTime() {
return createTime;
}
return System.currentTimeMillis() < retryUntil;
}
public long getLifespan() {
return lifespan;
}
private void log(@NonNull String message) {
log(message, null);
}
public int getMaxAttempts() {
return maxAttempts;
}
private void log(@NonNull String message, @Nullable Throwable t) {
Log.i(TAG, buildLog(getId(), message), t);
}
public long getMaxBackoff() {
return maxBackoff;
}
private void warn(@NonNull String message) {
warn(message, null);
}
public int getMaxInstances() {
return maxInstances;
}
private void warn(@NonNull String message, @Nullable Throwable t) {
Log.w(TAG, buildLog(getId(), message), t);
}
public @Nullable String getQueue() {
return queue;
}
private String buildLog(@NonNull UUID id, @NonNull String message) {
return "[" + id + "] " + getClass().getSimpleName() + " :: " + message;
}
public List<String> getConstraintKeys() {
return constraintKeys;
}
protected String logSuffix() {
long timeSinceSubmission = System.currentTimeMillis() - getInputData().getLong(KEY_SUBMIT_TIME, 0);
return " (Time since submission: " + timeSinceSubmission + " ms, Run attempt: " + getRunAttemptCount() + ", isStopped: " + isStopped() + ")";
public static final class Builder {
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);
}
}
}
}

View File

@ -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();
}
}

View File

@ -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.");
}
}
}

View File

@ -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);
}
}

View File

@ -1,138 +1,181 @@
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 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.util.Debouncer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkContinuation;
import androidx.work.WorkManager;
public class JobManager {
/**
* Allows the scheduling of durable jobs that will be run as early as possible.
*/
public class JobManager implements ConstraintObserver.Notifier {
private static final String TAG = JobManager.class.getSimpleName();
private static final Constraints NETWORK_CONSTRAINT = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
private final ExecutorService executor;
private final JobController jobController;
private final JobRunner[] jobRunners;
private final Executor executor = Executors.newSingleThreadExecutor();
private final Set<EmptyQueueListener> emptyQueueListeners = new CopyOnWriteArraySet<>();
private final Context context;
private final WorkManager workManager;
public JobManager(@NonNull Application application, @NonNull Configuration configuration) {
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(() -> {
try {
workManager.pruneWork().getResult().get();
} catch (ExecutionException | InterruptedException e) {
Log.w(TAG, "Failed to prune work.", e);
if (WorkManagerMigrator.needsMigration(application)) {
Log.i(TAG, "Detected an old WorkManager database. Migrating.");
WorkManagerMigrator.migrate(application, configuration.getJobStorage(), configuration.getDataSerializer());
}
List<List<Job>> jobListChain = chain.getJobListChain();
List<List<OneTimeWorkRequest>> requestListChain = Stream.of(jobListChain)
.filter(jobList -> !jobList.isEmpty())
.map(jobList -> Stream.of(jobList).map(this::toWorkRequest).toList())
.toList();
jobController.init();
if (jobListChain.isEmpty()) {
throw new IllegalStateException("Enqueued an empty chain.");
for (int i = 0; i < jobRunners.length; i++) {
jobRunners[i] = new JobRunner(application, i + 1, jobController);
jobRunners[i].start();
}
for (int i = 0; i < jobListChain.size(); i++) {
for (int j = 0; j < jobListChain.get(i).size(); j++) {
jobListChain.get(i).get(j).onSubmit(context, requestListChain.get(i).get(j).getId());
}
for (ConstraintObserver constraintObserver : configuration.getConstraintObservers()) {
constraintObserver.register(this);
}
WorkContinuation continuation;
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));
if (Build.VERSION.SDK_INT < 26) {
application.startService(new Intent(application, KeepAliveService.class));
}
for (int i = 1; i < requestListChain.size(); i++) {
continuation = continuation.then(requestListChain.get(i));
}
continuation.enqueue();
wakeUp();
});
}
private OneTimeWorkRequest toWorkRequest(@NonNull Job job) {
JobParameters jobParameters = job.getJobParameters();
if (jobParameters == null) {
throw new IllegalStateException("Jobs must have JobParameters at this stage. (" + job.getClass().getSimpleName() + ")");
}
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();
/**
* Enqueues a single job to be run.
*/
public void add(@NonNull Job job) {
new Chain(this, Collections.singletonList(job)).enqueue();
}
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));
}
@ -141,16 +184,146 @@ public class JobManager {
}
public Chain then(@NonNull List<Job> jobs) {
this.jobs.add(new ArrayList<>(jobs));
if (!jobs.isEmpty()) {
this.jobs.add(new ArrayList<>(jobs));
}
return this;
}
public void enqueue(@NonNull ChainParameters chainParameters) {
enqueueChain(this, chainParameters);
public void enqueue() {
jobManager.enqueueChain(this);
}
private List<List<Job>> getJobListChain() {
return jobs;
}
}
public static class Configuration {
private final ExecutorFactory executorFactory;
private final int jobThreadCount;
private final JobInstantiator jobInstantiator;
private final ConstraintInstantiator constraintInstantiator;
private final List<ConstraintObserver> constraintObservers;
private final Data.Serializer dataSerializer;
private final JobStorage jobStorage;
private final 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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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 {
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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) {
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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
}

View File

@ -1,23 +1,22 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
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.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.util.AttachmentUtil;
@ -37,11 +36,10 @@ import java.io.InputStream;
import javax.inject.Inject;
import androidx.work.Data;
import androidx.work.WorkerParameters;
public class AttachmentDownloadJob extends BaseJob implements InjectableType {
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 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_MANUAL = "part_manual";
@Inject transient SignalServiceMessageReceiver messageReceiver;
@Inject SignalServiceMessageReceiver messageReceiver;
private long messageId;
private long partRowId;
private long partUniqueId;
private boolean manual;
public AttachmentDownloadJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
super(context, workerParameters);
public AttachmentDownloadJob(long messageId, AttachmentId attachmentId, boolean manual) {
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) {
super(context, JobParameters.newBuilder()
.withGroupId(AttachmentDownloadJob.class.getSimpleName() + attachmentId.getRowId() + "-" + attachmentId.getUniqueId())
.withNetworkRequirement()
.create());
private AttachmentDownloadJob(@NonNull Job.Parameters parameters, long messageId, AttachmentId attachmentId, boolean manual) {
super(parameters);
this.messageId = messageId;
this.partRowId = attachmentId.getRowId();
@ -74,20 +77,17 @@ public class AttachmentDownloadJob extends ContextJob implements InjectableType
}
@Override
protected void initialize(@NonNull SafeData data) {
messageId = data.getLong(KEY_MESSAGE_ID);
partRowId = data.getLong(KEY_PART_ROW_ID);
partUniqueId = data.getLong(KEY_PAR_UNIQUE_ID);
manual = data.getBoolean(KEY_MANUAL);
public @NonNull Data serialize() {
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
.putLong(KEY_PART_ROW_ID, partRowId)
.putLong(KEY_PAR_UNIQUE_ID, partUniqueId)
.putBoolean(KEY_MANUAL, manual)
.build();
}
@Override
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
return dataBuilder.putLong(KEY_MESSAGE_ID, messageId)
.putLong(KEY_PART_ROW_ID, partRowId)
.putLong(KEY_PAR_UNIQUE_ID, partUniqueId)
.putBoolean(KEY_MANUAL, manual)
.build();
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
@ -242,4 +242,13 @@ public class AttachmentDownloadJob extends ContextJob implements InjectableType
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));
}
}
}

View File

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.support.annotation.NonNull;
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.dependencies.InjectableType;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
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.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.MediaStream;
@ -35,10 +35,9 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.net.ssl.SSLException;
import androidx.work.Data;
import androidx.work.WorkerParameters;
public class AttachmentUploadJob extends BaseJob implements InjectableType {
public class AttachmentUploadJob extends ContextJob implements InjectableType {
public static final String KEY = "AttachmentUploadJob";
private static final String TAG = AttachmentUploadJob.class.getSimpleName();
@ -48,29 +47,30 @@ public class AttachmentUploadJob extends ContextJob implements InjectableType {
private AttachmentId attachmentId;
@Inject SignalServiceMessageSender messageSender;
public AttachmentUploadJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
super(context, workerParameters);
public AttachmentUploadJob(AttachmentId attachmentId) {
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) {
super(context, new JobParameters.Builder()
.withNetworkRequirement()
.withRetryDuration(TimeUnit.DAYS.toMillis(1))
.create());
private AttachmentUploadJob(@NonNull Job.Parameters parameters, @NonNull AttachmentId attachmentId) {
super(parameters);
this.attachmentId = attachmentId;
}
@Override
protected void initialize(@NonNull SafeData data) {
this.attachmentId = new AttachmentId(data.getLong(KEY_ROW_ID), data.getLong(KEY_UNIQUE_ID));
public @NonNull Data serialize() {
return new Data.Builder().putLong(KEY_ROW_ID, attachmentId.getRowId())
.putLong(KEY_UNIQUE_ID, attachmentId.getUniqueId())
.build();
}
@Override
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
return dataBuilder.putLong(KEY_ROW_ID, attachmentId.getRowId())
.putLong(KEY_UNIQUE_ID, attachmentId.getUniqueId())
.build();
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
@ -92,7 +92,7 @@ public class AttachmentUploadJob extends ContextJob implements InjectableType {
}
@Override
protected void onCanceled() { }
public void onCanceled() { }
@Override
protected boolean onShouldRetry(Exception exception) {
@ -145,4 +145,11 @@ public class AttachmentUploadJob extends ContextJob implements InjectableType {
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)));
}
}
}

View File

@ -1,16 +1,15 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
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.mms.AttachmentStreamUriLoader.AttachmentModel;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
@ -29,46 +28,41 @@ import java.io.InputStream;
import javax.inject.Inject;
import androidx.work.Data;
import androidx.work.WorkerParameters;
public class AvatarDownloadJob extends BaseJob implements InjectableType {
public class AvatarDownloadJob extends ContextJob implements InjectableType {
private static final int MAX_AVATAR_SIZE = 20 * 1024 * 1024;
private static final long serialVersionUID = 1L;
public static final String KEY = "AvatarDownloadJob";
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";
@Inject transient SignalServiceMessageReceiver receiver;
@Inject SignalServiceMessageReceiver receiver;
private byte[] groupId;
public AvatarDownloadJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
super(context, workerParameters);
public AvatarDownloadJob(@NonNull byte[] groupId) {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(10)
.build(),
groupId);
}
public AvatarDownloadJob(Context context, @NonNull byte[] groupId) {
super(context, JobParameters.newBuilder()
.withNetworkRequirement()
.create());
private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull byte[] groupId) {
super(parameters);
this.groupId = groupId;
}
@Override
protected void initialize(@NonNull SafeData data) {
try {
groupId = GroupUtil.getDecodedId(data.getString(KEY_GROUP_ID));
} catch (IOException e) {
throw new AssertionError(e);
}
public @NonNull Data serialize() {
return new Data.Builder().putString(KEY_GROUP_ID, GroupUtil.getEncodedId(groupId, false)).build();
}
@Override
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
return dataBuilder.putString(KEY_GROUP_ID, GroupUtil.getEncodedId(groupId, false)).build();
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
@ -122,4 +116,14 @@ public class AvatarDownloadJob extends ContextJob implements InjectableType {
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);
}
}
}
}

View File

@ -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);
}

View File

@ -1,15 +1,13 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
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.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
@ -26,38 +24,38 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import androidx.work.Data;
import androidx.work.WorkerParameters;
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 long ARCHIVE_AGE = TimeUnit.DAYS.toMillis(7);
@Inject transient SignalServiceAccountManager accountManager;
@Inject transient SignedPreKeyStoreFactory signedPreKeyStoreFactory;
@Inject SignalServiceAccountManager accountManager;
@Inject SignedPreKeyStoreFactory signedPreKeyStoreFactory;
public CleanPreKeysJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
super(context, workerParameters);
public CleanPreKeysJob() {
this(new Job.Parameters.Builder()
.setQueue("CleanPreKeysJob")
.setMaxAttempts(5)
.build());
}
public CleanPreKeysJob(Context context) {
super(context, JobParameters.newBuilder()
.withGroupId(CleanPreKeysJob.class.getSimpleName())
.withRetryCount(5)
.create());
private CleanPreKeysJob(@NonNull Job.Parameters parameters) {
super(parameters);
}
@Override
protected void initialize(@NonNull SafeData data) {
public @NonNull Data serialize() {
return Data.EMPTY;
}
@Override
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
return dataBuilder.build();
public @NonNull String getFactoryKey() {
return KEY;
}
@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);
}
}
}

View File

@ -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;
}
}

View File

@ -4,11 +4,11 @@ import android.content.Context;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
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.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
@ -20,35 +20,35 @@ import java.io.IOException;
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();
@Inject transient SignalServiceAccountManager accountManager;
public CreateSignedPreKeyJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
super(context, workerParameters);
}
@Inject SignalServiceAccountManager accountManager;
public CreateSignedPreKeyJob(Context context) {
super(context, JobParameters.newBuilder()
.withNetworkRequirement()
.withGroupId(CreateSignedPreKeyJob.class.getSimpleName())
.create());
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setQueue("CreateSignedPreKeyJob")
.setMaxAttempts(25)
.build());
}
private CreateSignedPreKeyJob(@NonNull Job.Parameters parameters) {
super(parameters);
}
@Override
protected void initialize(@NonNull SafeData data) {
public @NonNull Data serialize() {
return Data.EMPTY;
}
@Override
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
return dataBuilder.build();
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
@ -78,4 +78,11 @@ public class CreateSignedPreKeyJob extends ContextJob implements InjectableType
if (exception instanceof PushNetworkException) return true;
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);
}
}
}

View File

@ -1,68 +1,66 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.app.Application;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
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.jobmanager.JobParameters;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.DirectoryHelper;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import androidx.work.Data;
import androidx.work.WorkerParameters;
public class DirectoryRefreshJob extends BaseJob {
public class DirectoryRefreshJob extends ContextJob {
public static final String KEY = "DirectoryRefreshJob";
private static final String TAG = DirectoryRefreshJob.class.getSimpleName();
private static final String KEY_ADDRESS = "address";
private static final String KEY_NOTIFY_OF_NEW_USERS = "notify_of_new_users";
@Nullable private transient Recipient recipient;
private transient boolean notifyOfNewUsers;
@Nullable private Recipient recipient;
private boolean notifyOfNewUsers;
public DirectoryRefreshJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
super(context, workerParameters);
public DirectoryRefreshJob(boolean notifyOfNewUsers) {
this(null, notifyOfNewUsers);
}
public DirectoryRefreshJob(@NonNull Context context, boolean notifyOfNewUsers) {
this(context, null, notifyOfNewUsers);
}
public DirectoryRefreshJob(@NonNull Context context,
@Nullable Recipient recipient,
boolean notifyOfNewUsers)
public DirectoryRefreshJob(@Nullable Recipient recipient,
boolean notifyOfNewUsers)
{
super(context, JobParameters.newBuilder()
.withGroupId(DirectoryRefreshJob.class.getSimpleName())
.withNetworkRequirement()
.create());
this(new Job.Parameters.Builder()
.setQueue("DirectoryRefreshJob")
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(10)
.build(),
recipient,
notifyOfNewUsers);
}
private DirectoryRefreshJob(@NonNull Job.Parameters parameters, @Nullable Recipient recipient, boolean notifyOfNewUsers) {
super(parameters);
this.recipient = recipient;
this.notifyOfNewUsers = notifyOfNewUsers;
}
@Override
protected void initialize(@NonNull SafeData data) {
String serializedAddress = data.getString(KEY_ADDRESS);
Address address = serializedAddress != null ? Address.fromSerialized(serializedAddress) : null;
recipient = address != null ? Recipient.from(context, address, true) : null;
notifyOfNewUsers = data.getBoolean(KEY_NOTIFY_OF_NEW_USERS);
public @NonNull Data serialize() {
return new Data.Builder().putString(KEY_ADDRESS, recipient != null ? recipient.getAddress().serialize() : null)
.putBoolean(KEY_NOTIFY_OF_NEW_USERS, notifyOfNewUsers)
.build();
}
@Override
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
return dataBuilder.putString(KEY_ADDRESS, recipient != null ? recipient.getAddress().serialize() : null)
.putBoolean(KEY_NOTIFY_OF_NEW_USERS, notifyOfNewUsers)
.build();
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
@ -84,4 +82,23 @@ public class DirectoryRefreshJob extends ContextJob {
@Override
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);
}
}
}

View File

@ -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();
}
}

View File

@ -28,13 +28,14 @@ import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
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.PlayServicesProblemActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.transport.RetryLaterException;
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 java.io.IOException;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import androidx.work.Data;
import androidx.work.WorkerParameters;
public class FcmRefreshJob extends BaseJob implements InjectableType {
public class FcmRefreshJob extends ContextJob implements InjectableType {
public static final String KEY = "FcmRefreshJob";
private static final String TAG = FcmRefreshJob.class.getSimpleName();
@Inject transient SignalServiceAccountManager textSecureAccountManager;
@Inject SignalServiceAccountManager textSecureAccountManager;
public FcmRefreshJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
super(context, workerParameters);
public FcmRefreshJob() {
this(new Job.Parameters.Builder()
.setQueue("FcmRefreshJob")
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(1)
.setLifespan(TimeUnit.MINUTES.toMillis(5))
.setMaxInstances(1)
.build());
}
public FcmRefreshJob(Context context) {
super(context, JobParameters.newBuilder()
.withGroupId(FcmRefreshJob.class.getSimpleName())
.withDuplicatesIgnored(true)
.withNetworkRequirement()
.withRetryCount(1)
.create());
private FcmRefreshJob(@NonNull Job.Parameters parameters) {
super(parameters);
}
@Override
protected void initialize(@NonNull SafeData data) {
public @NonNull Data serialize() {
return Data.EMPTY;
}
@Override
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
return dataBuilder.build();
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
@ -139,4 +142,10 @@ public class FcmRefreshJob extends ContextJob implements InjectableType {
.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);
}
}
}

View File

@ -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());
}
}

View File

@ -2,11 +2,11 @@ package org.thoughtcrime.securesms.jobs;
import android.Manifest;
import android.content.Context;
import android.support.annotation.NonNull;
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.R;
@ -14,13 +14,11 @@ import org.thoughtcrime.securesms.backup.FullBackupExporter;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.io.File;
import java.io.IOException;
@ -28,31 +26,32 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import androidx.work.Data;
import androidx.work.WorkerParameters;
public class LocalBackupJob extends BaseJob {
public class LocalBackupJob extends ContextJob {
public static final String KEY = "LocalBackupJob";
private static final String TAG = LocalBackupJob.class.getSimpleName();
public LocalBackupJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
super(context, workerParameters);
public LocalBackupJob() {
this(new Job.Parameters.Builder()
.setQueue("__LOCAL_BACKUP__")
.setMaxInstances(1)
.setMaxAttempts(3)
.build());
}
public LocalBackupJob(@NonNull Context context) {
super(context, JobParameters.newBuilder()
.withGroupId("__LOCAL_BACKUP__")
.withDuplicatesIgnored(true)
.create());
private LocalBackupJob(@NonNull Job.Parameters parameters) {
super(parameters);
}
@Override
protected void initialize(@NonNull SafeData data) {
public @NonNull Data serialize() {
return Data.EMPTY;
}
@Override
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
return dataBuilder.build();
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
@ -109,6 +108,12 @@ public class LocalBackupJob extends ContextJob {
@Override
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);
}
}
}

View File

@ -1,11 +1,11 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
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 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.MessagingDatabase.InsertResult;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
import org.thoughtcrime.securesms.mms.ApnUnavailableException;
import org.thoughtcrime.securesms.mms.CompatMmsConnection;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
@ -46,12 +45,9 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import androidx.work.Data;
import androidx.work.WorkerParameters;
public class MmsDownloadJob extends BaseJob {
public class MmsDownloadJob extends ContextJob {
private static final long serialVersionUID = 1L;
public static final String KEY = "MmsDownloadJob";
private static final String TAG = MmsDownloadJob.class.getSimpleName();
@ -63,14 +59,19 @@ public class MmsDownloadJob extends ContextJob {
private long threadId;
private boolean automatic;
public MmsDownloadJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
super(context, workerParameters);
public MmsDownloadJob(long messageId, long threadId, boolean automatic) {
this(new Job.Parameters.Builder()
.setQueue("mms-operation")
.setMaxAttempts(25)
.build(),
messageId,
threadId,
automatic);
}
public MmsDownloadJob(Context context, long messageId, long threadId, boolean automatic) {
super(context, JobParameters.newBuilder()
.withGroupId("mms-operation")
.create());
private MmsDownloadJob(@NonNull Job.Parameters parameters, long messageId, long threadId, boolean automatic) {
super(parameters);
this.messageId = messageId;
this.threadId = threadId;
@ -78,18 +79,16 @@ public class MmsDownloadJob extends ContextJob {
}
@Override
protected void initialize(@NonNull SafeData data) {
messageId = data.getLong(KEY_MESSAGE_ID);
threadId = data.getLong(KEY_THREAD_ID);
automatic = data.getBoolean(KEY_AUTOMATIC);
public @NonNull Data serialize() {
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
.putLong(KEY_THREAD_ID, threadId)
.putBoolean(KEY_AUTOMATIC, automatic)
.build();
}
@Override
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
return dataBuilder.putLong(KEY_MESSAGE_ID, messageId)
.putLong(KEY_THREAD_ID, threadId)
.putBoolean(KEY_AUTOMATIC, automatic)
.build();
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
@ -269,4 +268,14 @@ public class MmsDownloadJob extends ContextJob {
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));
}
}
}

View File

@ -1,8 +1,7 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
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 android.support.annotation.NonNull;
@ -17,19 +16,15 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import androidx.work.Data;
import androidx.work.WorkerParameters;
public class MmsReceiveJob extends BaseJob {
public class MmsReceiveJob extends ContextJob {
private static final long serialVersionUID = 1L;
public static final String KEY = "MmsReceiveJob";
private static final String TAG = MmsReceiveJob.class.getSimpleName();
@ -39,32 +34,27 @@ public class MmsReceiveJob extends ContextJob {
private byte[] data;
private int subscriptionId;
public MmsReceiveJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
super(context, workerParameters);
public MmsReceiveJob(byte[] data, int subscriptionId) {
this(new Job.Parameters.Builder().setMaxAttempts(25).build(), data, subscriptionId);
}
public MmsReceiveJob(Context context, byte[] data, int subscriptionId) {
super(context, JobParameters.newBuilder().create());
private MmsReceiveJob(@NonNull Job.Parameters parameters, byte[] data, int subscriptionId) {
super(parameters);
this.data = data;
this.subscriptionId = subscriptionId;
}
@Override
protected void initialize(@NonNull SafeData data) {
try {
this.data = Base64.decode(data.getString(KEY_DATA));
} catch (IOException e) {
throw new AssertionError(e);
}
subscriptionId = data.getInt(KEY_SUBSCRIPTION_ID);
public @NonNull Data serialize() {
return new Data.Builder().putString(KEY_DATA, Base64.encodeBytes(data))
.putInt(KEY_SUBSCRIPTION_ID, subscriptionId)
.build();
}
@Override
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
return dataBuilder.putString(KEY_DATA, Base64.encodeBytes(data))
.putInt(KEY_SUBSCRIPTION_ID, subscriptionId)
.build();
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
@ -91,8 +81,7 @@ public class MmsReceiveJob extends ContextJob {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new MmsDownloadJob(context,
messageAndThreadId.first,
.add(new MmsDownloadJob(messageAndThreadId.first,
messageAndThreadId.second,
true));
} else if (isNotification(pdu)) {
@ -122,4 +111,15 @@ public class MmsReceiveJob extends ContextJob {
private boolean isNotification(GenericPdu pdu) {
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);
}
}
}
}

View File

@ -4,7 +4,9 @@ import android.content.Context;
import android.support.annotation.NonNull;
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 android.webkit.MimeTypeMap;
@ -28,7 +30,6 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
import org.thoughtcrime.securesms.mms.CompatMmsConnection;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.MmsException;
@ -49,12 +50,9 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import androidx.work.Data;
import androidx.work.WorkerParameters;
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();
@ -62,28 +60,28 @@ public class MmsSendJob extends SendJob {
private long messageId;
public MmsSendJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
super(context, workerParameters);
public MmsSendJob(long messageId) {
this(new Job.Parameters.Builder()
.setQueue("mms-operation")
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(15)
.build(),
messageId);
}
public MmsSendJob(Context context, long messageId) {
super(context, JobParameters.newBuilder()
.withGroupId("mms-operation")
.withNetworkRequirement()
.withRetryCount(15)
.create());
private MmsSendJob(@NonNull Job.Parameters parameters, long messageId) {
super(parameters);
this.messageId = messageId;
}
@Override
protected void initialize(@NonNull SafeData data) {
messageId = data.getLong(KEY_MESSAGE_ID);
public @NonNull Data serialize() {
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId).build();
}
@Override
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
return dataBuilder.putLong(KEY_MESSAGE_ID, messageId).build();
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
@ -318,4 +316,11 @@ public class MmsSendJob extends SendJob {
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