From 4a3c173adb45d6371855c851fdf806b90db7950a Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 28 Mar 2019 08:56:35 -0700 Subject: [PATCH] Migrated to new JobManager. --- AndroidManifest.xml | 23 + build.gradle | 9 +- proguard-workmanager.pro | 2 - res/values-v26/values.xml | 5 + res/values/values.xml | 5 + .../securesms/ApplicationContext.java | 27 +- .../securesms/ConversationListFragment.java | 4 +- .../securesms/CreateProfileActivity.java | 2 +- .../securesms/DatabaseUpgradeActivity.java | 41 +- .../securesms/DeviceListFragment.java | 2 +- .../securesms/LinkPreviewsIntroFragment.java | 4 +- .../PassphraseRequiredActionBarActivity.java | 1 - .../securesms/ReadReceiptsIntroFragment.java | 3 +- .../RecipientPreferenceActivity.java | 4 +- .../securesms/RegistrationActivity.java | 3 +- .../TypingIndicatorIntroFragment.java | 3 +- .../securesms/VerifyIdentityActivity.java | 3 +- .../components/TypingStatusSender.java | 4 +- .../components/camera/CameraView.java | 3 - .../SharedContactDetailsActivity.java | 2 +- .../conversation/ConversationActivity.java | 6 +- .../conversation/ConversationFragment.java | 2 +- .../conversation/ConversationItem.java | 6 +- .../securesms/database/DatabaseFactory.java | 6 + .../securesms/database/JobDatabase.java | 242 ++++++ .../securesms/database/MmsDatabase.java | 10 +- .../securesms/database/SmsDatabase.java | 12 +- .../database/helpers/SQLCipherOpenHelper.java | 37 +- .../SignalCommunicationModule.java | 2 - .../securesms/gcm/FcmService.java | 7 +- .../groups/GroupMessageProcessor.java | 4 +- .../jobmanager/AlarmManagerScheduler.java | 67 ++ .../securesms/jobmanager/BootReceiver.java | 17 + .../securesms/jobmanager/ChainParameters.java | 45 -- .../jobmanager/CompositeScheduler.java | 22 + .../securesms/jobmanager/Constraint.java | 19 + .../jobmanager/ConstraintInstantiator.java | 23 + .../jobmanager/ConstraintObserver.java | 12 + .../securesms/jobmanager/Data.java | 307 ++++++++ .../DependencyInjector.java | 4 +- .../securesms/jobmanager/EncryptionKeys.java | 30 - .../securesms/jobmanager/ExecutorFactory.java | 9 + .../securesms/jobmanager/InAppScheduler.java | 49 ++ .../securesms/jobmanager/Job.java | 423 +++++----- .../securesms/jobmanager/JobController.java | 353 +++++++++ .../securesms/jobmanager/JobInstantiator.java | 24 + .../securesms/jobmanager/JobLogger.java | 24 + .../securesms/jobmanager/JobManager.java | 379 ++++++--- .../securesms/jobmanager/JobParameters.java | 202 ----- .../securesms/jobmanager/JobRunner.java | 110 +++ .../jobmanager/JobSchedulerScheduler.java | 90 +++ .../jobmanager/KeepAliveService.java | 24 + .../securesms/jobmanager/SafeData.java | 55 -- .../securesms/jobmanager/Scheduler.java | 9 + .../securesms/jobmanager/WorkLockManager.java | 94 --- .../dependencies/ContextDependent.java | 27 - .../impl/CellServiceConstraint.java | 48 ++ .../impl/CellServiceConstraintObserver.java | 38 + .../impl/DefaultExecutorFactory.java | 15 + .../jobmanager/impl/JsonDataSerializer.java | 34 + .../jobmanager/impl/NetworkConstraint.java | 55 ++ .../impl/NetworkConstraintObserver.java | 36 + .../impl/NetworkOrCellServiceConstraint.java | 48 ++ .../impl/SqlCipherMigrationConstraint.java | 48 ++ .../SqlCipherMigrationConstraintObserver.java | 32 + .../jobmanager/migration/DataMigrator.java | 94 +++ .../migration/WorkManagerDatabase.java | 101 +++ .../migration/WorkManagerFactoryMappings.java | 104 +++ .../migration/WorkManagerMigrator.java | 45 ++ .../persistence/ConstraintSpec.java | 43 + .../persistence/DependencySpec.java | 43 + .../jobmanager/persistence/FullSpec.java | 50 ++ .../persistence/JavaJobSerializer.java | 66 -- .../jobmanager/persistence/JobSerializer.java | 47 -- .../jobmanager/persistence/JobSpec.java | 129 +++ .../jobmanager/persistence/JobStorage.java | 55 ++ .../persistence/PersistentStorage.java | 107 --- .../NetworkBackoffRequirement.java | 44 -- .../requirements/NetworkRequirement.java | 50 -- .../NetworkRequirementProvider.java | 53 -- .../jobmanager/requirements/Requirement.java | 35 - .../requirements/RequirementListener.java | 21 - .../requirements/RequirementProvider.java | 33 - .../requirements/SimpleRequirement.java | 19 - .../securesms/jobmanager/util/Base64.java | 741 ------------------ .../securesms/jobs/AttachmentDownloadJob.java | 63 +- .../securesms/jobs/AttachmentUploadJob.java | 49 +- .../securesms/jobs/AvatarDownloadJob.java | 56 +- .../thoughtcrime/securesms/jobs/BaseJob.java | 36 + .../securesms/jobs/CleanPreKeysJob.java | 44 +- .../securesms/jobs/ContextJob.java | 32 - .../securesms/jobs/CreateSignedPreKeyJob.java | 45 +- .../securesms/jobs/DirectoryRefreshJob.java | 79 +- .../securesms/jobs/FastJobStorage.java | 259 ++++++ .../securesms/jobs/FcmRefreshJob.java | 45 +- .../securesms/jobs/JobManagerFactories.java | 86 ++ .../securesms/jobs/LocalBackupJob.java | 39 +- .../securesms/jobs/MmsDownloadJob.java | 55 +- .../securesms/jobs/MmsReceiveJob.java | 52 +- .../securesms/jobs/MmsSendJob.java | 43 +- .../jobs/MultiDeviceBlockedUpdateJob.java | 46 +- .../MultiDeviceConfigurationUpdateJob.java | 75 +- .../jobs/MultiDeviceContactUpdateJob.java | 57 +- .../jobs/MultiDeviceGroupUpdateJob.java | 46 +- .../jobs/MultiDeviceProfileKeyUpdateJob.java | 52 +- .../jobs/MultiDeviceReadReceiptUpdateJob.java | 86 -- .../jobs/MultiDeviceReadUpdateJob.java | 77 +- .../jobs/MultiDeviceVerifiedUpdateJob.java | 87 +- .../securesms/jobs/PushContentReceiveJob.java | 42 +- .../securesms/jobs/PushDecryptJob.java | 82 +- .../securesms/jobs/PushGroupSendJob.java | 75 +- .../securesms/jobs/PushGroupUpdateJob.java | 62 +- .../securesms/jobs/PushMediaSendJob.java | 47 +- .../jobs/PushNotificationReceiveJob.java | 46 +- .../securesms/jobs/PushReceivedJob.java | 17 +- .../securesms/jobs/PushSendJob.java | 35 +- .../securesms/jobs/PushTextSendJob.java | 37 +- .../securesms/jobs/RefreshAttributesJob.java | 45 +- .../securesms/jobs/RefreshPreKeysJob.java | 46 +- ...RefreshUnidentifiedDeliveryAbilityJob.java | 44 +- .../securesms/jobs/RequestGroupInfoJob.java | 66 +- .../jobs/RetrieveProfileAvatarJob.java | 61 +- .../securesms/jobs/RetrieveProfileJob.java | 54 +- .../securesms/jobs/RotateCertificateJob.java | 51 +- .../securesms/jobs/RotateProfileKeyJob.java | 53 +- .../securesms/jobs/RotateSignedPreKeyJob.java | 44 +- .../jobs/SendDeliveryReceiptJob.java | 66 +- .../thoughtcrime/securesms/jobs/SendJob.java | 22 +- .../securesms/jobs/SendReadReceiptJob.java | 82 +- .../jobs/ServiceOutageDetectionJob.java | 43 +- .../securesms/jobs/SmsReceiveJob.java | 74 +- .../securesms/jobs/SmsSendJob.java | 83 +- .../securesms/jobs/SmsSentJob.java | 62 +- .../securesms/jobs/TrimThreadJob.java | 34 +- .../securesms/jobs/TypingSendJob.java | 52 +- .../securesms/jobs/UpdateApkJob.java | 42 +- .../requirements/MasterSecretRequirement.java | 27 - .../MasterSecretRequirementProvider.java | 36 - .../NetworkOrServiceRequirement.java | 30 - .../jobs/requirements/ServiceRequirement.java | 30 - .../ServiceRequirementProvider.java | 56 -- .../SqlCipherMigrationRequirement.java | 32 - ...SqlCipherMigrationRequirementProvider.java | 31 - .../logsubmit/SubmitLogFragment.java | 6 +- .../notifications/MarkReadReceiver.java | 4 +- .../AppProtectionPreferenceFragment.java | 17 +- .../preferences/ChatsPreferenceFragment.java | 2 +- .../securesms/service/BootReceiver.java | 1 - .../service/DirectoryRefreshListener.java | 2 +- .../service/IncomingMessageObserver.java | 22 +- .../service/LocalBackupListener.java | 2 +- .../securesms/service/MmsListener.java | 4 +- .../service/RotateSignedPreKeyListener.java | 2 +- .../service/SmsDeliveryListener.java | 13 +- .../securesms/service/SmsListener.java | 4 +- .../service/UpdateApkRefreshListener.java | 2 +- .../securesms/sms/MessageSender.java | 6 +- .../securesms/util/TextSecurePreferences.java | 5 +- .../impl/JsonDataSerializerTest.java | 47 ++ .../securesms/jobs/FastJobStorageTest.java | 352 +++++++++ .../securesms/testutil/DirectExecutor.java | 16 + .../resources/data/data_serialized.json | 73 ++ 162 files changed, 5360 insertions(+), 3574 deletions(-) delete mode 100644 proguard-workmanager.pro create mode 100644 res/values-v26/values.xml create mode 100644 res/values/values.xml create mode 100644 src/org/thoughtcrime/securesms/database/JobDatabase.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/AlarmManagerScheduler.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/BootReceiver.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/ChainParameters.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/CompositeScheduler.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/Constraint.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/ConstraintInstantiator.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/ConstraintObserver.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/Data.java rename src/org/thoughtcrime/securesms/jobmanager/{dependencies => }/DependencyInjector.java (88%) delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/EncryptionKeys.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/ExecutorFactory.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/InAppScheduler.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/JobController.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/JobInstantiator.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/JobLogger.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/JobParameters.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/JobRunner.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/JobSchedulerScheduler.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/KeepAliveService.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/SafeData.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/Scheduler.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/WorkLockManager.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/dependencies/ContextDependent.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/impl/CellServiceConstraint.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/impl/CellServiceConstraintObserver.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/impl/DefaultExecutorFactory.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/impl/JsonDataSerializer.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/impl/NetworkConstraint.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/impl/NetworkConstraintObserver.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/impl/NetworkOrCellServiceConstraint.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/impl/SqlCipherMigrationConstraint.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/impl/SqlCipherMigrationConstraintObserver.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/migration/DataMigrator.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerDatabase.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerMigrator.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/persistence/ConstraintSpec.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/persistence/DependencySpec.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/persistence/FullSpec.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/persistence/JavaJobSerializer.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/persistence/JobSerializer.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/persistence/JobSpec.java create mode 100644 src/org/thoughtcrime/securesms/jobmanager/persistence/JobStorage.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/persistence/PersistentStorage.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkBackoffRequirement.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkRequirement.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkRequirementProvider.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/requirements/Requirement.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/requirements/RequirementListener.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/requirements/RequirementProvider.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/requirements/SimpleRequirement.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/util/Base64.java create mode 100644 src/org/thoughtcrime/securesms/jobs/BaseJob.java delete mode 100644 src/org/thoughtcrime/securesms/jobs/ContextJob.java create mode 100644 src/org/thoughtcrime/securesms/jobs/FastJobStorage.java create mode 100644 src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java delete mode 100644 src/org/thoughtcrime/securesms/jobs/MultiDeviceReadReceiptUpdateJob.java delete mode 100644 src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirement.java delete mode 100644 src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirementProvider.java delete mode 100644 src/org/thoughtcrime/securesms/jobs/requirements/NetworkOrServiceRequirement.java delete mode 100644 src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirement.java delete mode 100644 src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirementProvider.java delete mode 100644 src/org/thoughtcrime/securesms/jobs/requirements/SqlCipherMigrationRequirement.java delete mode 100644 src/org/thoughtcrime/securesms/jobs/requirements/SqlCipherMigrationRequirementProvider.java create mode 100644 test/unitTest/java/org/thoughtcrime/securesms/jobmanager/impl/JsonDataSerializerTest.java create mode 100644 test/unitTest/java/org/thoughtcrime/securesms/jobs/FastJobStorageTest.java create mode 100644 test/unitTest/java/org/thoughtcrime/securesms/testutil/DirectExecutor.java create mode 100644 test/unitTest/resources/data/data_serialized.json diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 109c9e57b..1620f9dab 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -659,6 +659,29 @@ + + + + + + + + + + + + + diff --git a/build.gradle b/build.gradle index 04e0627ac..019591820 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/proguard-workmanager.pro b/proguard-workmanager.pro deleted file mode 100644 index bfd4583fd..000000000 --- a/proguard-workmanager.pro +++ /dev/null @@ -1,2 +0,0 @@ --dontwarn sun.misc.Unsafe --dontwarn com.google.common.util.concurrent.ListenableFuture diff --git a/res/values-v26/values.xml b/res/values-v26/values.xml new file mode 100644 index 000000000..dc6b4b794 --- /dev/null +++ b/res/values-v26/values.xml @@ -0,0 +1,5 @@ + + + false + true + diff --git a/res/values/values.xml b/res/values/values.xml new file mode 100644 index 000000000..369e27339 --- /dev/null +++ b/res/values/values.xml @@ -0,0 +1,5 @@ + + + true + false + diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index b8b0b0cf8..47725f019 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -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()); } } diff --git a/src/org/thoughtcrime/securesms/ConversationListFragment.java b/src/org/thoughtcrime/securesms/ConversationListFragment.java index 4748f04e7..8957eef76 100644 --- a/src/org/thoughtcrime/securesms/ConversationListFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationListFragment.java @@ -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)); diff --git a/src/org/thoughtcrime/securesms/CreateProfileActivity.java b/src/org/thoughtcrime/securesms/CreateProfileActivity.java index 7053451a4..bdd8f56f5 100644 --- a/src/org/thoughtcrime/securesms/CreateProfileActivity.java +++ b/src/org/thoughtcrime/securesms/CreateProfileActivity.java @@ -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; } diff --git a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java index 1a8590f2a..2289a6db0 100644 --- a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java +++ b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java @@ -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(); } diff --git a/src/org/thoughtcrime/securesms/DeviceListFragment.java b/src/org/thoughtcrime/securesms/DeviceListFragment.java index 461d2eb99..d200457f5 100644 --- a/src/org/thoughtcrime/securesms/DeviceListFragment.java +++ b/src/org/thoughtcrime/securesms/DeviceListFragment.java @@ -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(); diff --git a/src/org/thoughtcrime/securesms/LinkPreviewsIntroFragment.java b/src/org/thoughtcrime/securesms/LinkPreviewsIntroFragment.java index 632f74868..1016e4a91 100644 --- a/src/org/thoughtcrime/securesms/LinkPreviewsIntroFragment.java +++ b/src/org/thoughtcrime/securesms/LinkPreviewsIntroFragment.java @@ -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()))); diff --git a/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java b/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java index 02d170622..940ea8a9d 100644 --- a/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java +++ b/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java @@ -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; diff --git a/src/org/thoughtcrime/securesms/ReadReceiptsIntroFragment.java b/src/org/thoughtcrime/securesms/ReadReceiptsIntroFragment.java index 0d62ef5da..b154fb98f 100644 --- a/src/org/thoughtcrime/securesms/ReadReceiptsIntroFragment.java +++ b/src/org/thoughtcrime/securesms/ReadReceiptsIntroFragment.java @@ -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()))); diff --git a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java index be8a53eab..0b9078a88 100644 --- a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java +++ b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java @@ -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); diff --git a/src/org/thoughtcrime/securesms/RegistrationActivity.java b/src/org/thoughtcrime/securesms/RegistrationActivity.java index c23396497..75aa555c7 100644 --- a/src/org/thoughtcrime/securesms/RegistrationActivity.java +++ b/src/org/thoughtcrime/securesms/RegistrationActivity.java @@ -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); diff --git a/src/org/thoughtcrime/securesms/TypingIndicatorIntroFragment.java b/src/org/thoughtcrime/securesms/TypingIndicatorIntroFragment.java index cb92db73b..e783ba30d 100644 --- a/src/org/thoughtcrime/securesms/TypingIndicatorIntroFragment.java +++ b/src/org/thoughtcrime/securesms/TypingIndicatorIntroFragment.java @@ -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()))); diff --git a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java index f9e6337f9..db9a76d3e 100644 --- a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java +++ b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java @@ -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)); diff --git a/src/org/thoughtcrime/securesms/components/TypingStatusSender.java b/src/org/thoughtcrime/securesms/components/TypingStatusSender.java index 5281fe2a4..5c26d910e 100644 --- a/src/org/thoughtcrime/securesms/components/TypingStatusSender.java +++ b/src/org/thoughtcrime/securesms/components/TypingStatusSender.java @@ -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 { diff --git a/src/org/thoughtcrime/securesms/components/camera/CameraView.java b/src/org/thoughtcrime/securesms/components/camera/CameraView.java index 18a24397e..082896e02 100644 --- a/src/org/thoughtcrime/securesms/components/camera/CameraView.java +++ b/src/org/thoughtcrime/securesms/components/camera/CameraView.java @@ -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; diff --git a/src/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java b/src/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java index 9b9eea863..178364a75 100644 --- a/src/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java +++ b/src/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java @@ -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)); } } } diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 65d588819..1abb92c4b 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -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 diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java index a35d2801b..9f7cbb213 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -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)); } } diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java index a825994d9..e05286b0c 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -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()); diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index 8709e486d..493558498 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -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, diff --git a/src/org/thoughtcrime/securesms/database/JobDatabase.java b/src/org/thoughtcrime/securesms/database/JobDatabase.java new file mode 100644 index 000000000..6b5f4f15e --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/JobDatabase.java @@ -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 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 getAllJobSpecs() { + List 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 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 getAllConstraintSpecs() { + List 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 getAllDependencySpecs() { + List 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 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 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))); + } +} diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index 3184ebdcf..8e0f53e46 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -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; } diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java index 4be301d9b..9db2be22b 100644 --- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -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; diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 609ce7afd..4b7b9eba2 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -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(); diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index fe2e3e031..0b3d82c79 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -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, diff --git a/src/org/thoughtcrime/securesms/gcm/FcmService.java b/src/org/thoughtcrime/securesms/gcm/FcmService.java index bb4650cab..e7dd5ac17 100644 --- a/src/org/thoughtcrime/securesms/gcm/FcmService.java +++ b/src/org/thoughtcrime/securesms/gcm/FcmService.java @@ -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); diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index 8d450a8f4..4033a8823 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -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 { diff --git a/src/org/thoughtcrime/securesms/jobmanager/AlarmManagerScheduler.java b/src/org/thoughtcrime/securesms/jobmanager/AlarmManagerScheduler.java new file mode 100644 index 000000000..7d7722c7a --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/AlarmManagerScheduler.java @@ -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 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(); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/BootReceiver.java b/src/org/thoughtcrime/securesms/jobmanager/BootReceiver.java new file mode 100644 index 000000000..5e7d098d4 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/BootReceiver.java @@ -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."); + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/ChainParameters.java b/src/org/thoughtcrime/securesms/jobmanager/ChainParameters.java deleted file mode 100644 index 03fd00638..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/ChainParameters.java +++ /dev/null @@ -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 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); - } - } -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/CompositeScheduler.java b/src/org/thoughtcrime/securesms/jobmanager/CompositeScheduler.java new file mode 100644 index 000000000..f4844651a --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/CompositeScheduler.java @@ -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 schedulers; + + CompositeScheduler(@NonNull Scheduler... schedulers) { + this.schedulers = Arrays.asList(schedulers); + } + + @Override + public void schedule(long delay, @NonNull List constraints) { + for (Scheduler scheduler : schedulers) { + scheduler.schedule(delay, constraints); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/Constraint.java b/src/org/thoughtcrime/securesms/jobmanager/Constraint.java new file mode 100644 index 000000000..5ee9b7ece --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/Constraint.java @@ -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 create(); + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/ConstraintInstantiator.java b/src/org/thoughtcrime/securesms/jobmanager/ConstraintInstantiator.java new file mode 100644 index 000000000..2b41f4051 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/ConstraintInstantiator.java @@ -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 constraintFactories; + + ConstraintInstantiator(@NonNull Map 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."); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/ConstraintObserver.java b/src/org/thoughtcrime/securesms/jobmanager/ConstraintObserver.java new file mode 100644 index 000000000..f870496e6 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/ConstraintObserver.java @@ -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); + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/Data.java b/src/org/thoughtcrime/securesms/jobmanager/Data.java new file mode 100644 index 000000000..f7f77b0aa --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/Data.java @@ -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 strings; + @JsonProperty private final Map stringArrays; + @JsonProperty private final Map integers; + @JsonProperty private final Map integerArrays; + @JsonProperty private final Map longs; + @JsonProperty private final Map longArrays; + @JsonProperty private final Map floats; + @JsonProperty private final Map floatArrays; + @JsonProperty private final Map doubles; + @JsonProperty private final Map doubleArrays; + @JsonProperty private final Map booleans; + @JsonProperty private final Map booleanArrays; + + public Data(@JsonProperty("strings") @NonNull Map strings, + @JsonProperty("stringArrays") @NonNull Map stringArrays, + @JsonProperty("integers") @NonNull Map integers, + @JsonProperty("integerArrays") @NonNull Map integerArrays, + @JsonProperty("longs") @NonNull Map longs, + @JsonProperty("longArrays") @NonNull Map longArrays, + @JsonProperty("floats") @NonNull Map floats, + @JsonProperty("floatArrays") @NonNull Map floatArrays, + @JsonProperty("doubles") @NonNull Map doubles, + @JsonProperty("doubleArrays") @NonNull Map doubleArrays, + @JsonProperty("booleans") @NonNull Map booleans, + @JsonProperty("booleanArrays") @NonNull Map 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 strings = new HashMap<>(); + private final Map stringArrays = new HashMap<>(); + private final Map integers = new HashMap<>(); + private final Map integerArrays = new HashMap<>(); + private final Map longs = new HashMap<>(); + private final Map longArrays = new HashMap<>(); + private final Map floats = new HashMap<>(); + private final Map floatArrays = new HashMap<>(); + private final Map doubles = new HashMap<>(); + private final Map doubleArrays = new HashMap<>(); + private final Map booleans = new HashMap<>(); + private final Map 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); + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/dependencies/DependencyInjector.java b/src/org/thoughtcrime/securesms/jobmanager/DependencyInjector.java similarity index 88% rename from src/org/thoughtcrime/securesms/jobmanager/dependencies/DependencyInjector.java rename to src/org/thoughtcrime/securesms/jobmanager/DependencyInjector.java index 42e1ae3ad..c8a266bd8 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/dependencies/DependencyInjector.java +++ b/src/org/thoughtcrime/securesms/jobmanager/DependencyInjector.java @@ -14,11 +14,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -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); } diff --git a/src/org/thoughtcrime/securesms/jobmanager/EncryptionKeys.java b/src/org/thoughtcrime/securesms/jobmanager/EncryptionKeys.java deleted file mode 100644 index 588c6ca36..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/EncryptionKeys.java +++ /dev/null @@ -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 . - */ -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; - } -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/ExecutorFactory.java b/src/org/thoughtcrime/securesms/jobmanager/ExecutorFactory.java new file mode 100644 index 000000000..a86c99135 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/ExecutorFactory.java @@ -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); +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/InAppScheduler.java b/src/org/thoughtcrime/securesms/jobmanager/InAppScheduler.java new file mode 100644 index 000000000..585b262ad --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/InAppScheduler.java @@ -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 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); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/Job.java b/src/org/thoughtcrime/securesms/jobmanager/Job.java index a68d2a8b4..702a8f4bb 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/Job.java +++ b/src/org/thoughtcrime/securesms/jobmanager/Job.java @@ -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 { + @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 constraintKeys; + + private Parameters(long createTime, + long lifespan, + int maxAttempts, + long maxBackoff, + int maxInstances, + @Nullable String queue, + @NonNull List 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 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 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 constraintKeys) { + this.constraintKeys.clear(); + this.constraintKeys.addAll(constraintKeys); + return this; + } + + public @NonNull Parameters build() { + return new Parameters(createTime, lifespan, maxAttempts, maxBackoff, maxInstances, queue, constraintKeys); + } + } } } diff --git a/src/org/thoughtcrime/securesms/jobmanager/JobController.java b/src/org/thoughtcrime/securesms/jobmanager/JobController.java new file mode 100644 index 000000000..ff6a1be79 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/JobController.java @@ -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 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> 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 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 onFailure(@NonNull Job job) { + List dependents = Stream.of(jobStorage.getDependencySpecsThatDependOnJob(job.getId())) + .map(DependencySpec::getJobId) + .map(jobStorage::getJobSpec) + .withoutNulls() + .map(jobSpec -> { + List constraintSpecs = jobStorage.getConstraintSpecs(jobSpec.getId()); + return createJob(jobSpec, constraintSpecs); + }) + .toList(); + + List 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 jobs = jobStorage.getAllJobSpecs(); + List constraints = jobStorage.getAllConstraintSpecs(); + List 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> 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> chain) { + Stream.of(chain) + .forEach(list -> Stream.of(list).forEach(job -> { + job.setContext(application); + job.onSubmit(); + })); + } + + @WorkerThread + private void insertJobChain(@NonNull List> chain) { + List fullSpecs = new LinkedList<>(); + List dependsOn = Collections.emptyList(); + + for (List 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 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 constraintSpecs = Stream.of(job.getParameters().getConstraintKeys()) + .map(key -> new ConstraintSpec(jobSpec.getId(), key)) + .toList(); + + List 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 jobs) { + for (Job job : jobs) { + List 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 jobSpecs = jobStorage.getPendingJobsWithNoDependenciesInCreatedOrder(System.currentTimeMillis()); + + for (JobSpec jobSpec : jobSpecs) { + List constraintSpecs = jobStorage.getConstraintSpecs(jobSpec.getId()); + List 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 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 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(); + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/JobInstantiator.java b/src/org/thoughtcrime/securesms/jobmanager/JobInstantiator.java new file mode 100644 index 000000000..7fa716f3e --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/JobInstantiator.java @@ -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 jobFactories; + + JobInstantiator(@NonNull Map 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."); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/JobLogger.java b/src/org/thoughtcrime/securesms/jobmanager/JobLogger.java new file mode 100644 index 000000000..dd5e612f5 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/JobLogger.java @@ -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); + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/JobManager.java b/src/org/thoughtcrime/securesms/jobmanager/JobManager.java index fee756564..64c442237 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/JobManager.java +++ b/src/org/thoughtcrime/securesms/jobmanager/JobManager.java @@ -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 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 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> jobListChain = chain.getJobListChain(); - List> 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> 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 jobs) { + return new Chain(this, jobs); + } + + /** + * Retrieves a string representing the state of the job queue. Intended for debugging. + */ + public @NonNull String getDebugInfo() { + Future 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> jobs; + + private Chain(@NonNull JobManager jobManager, @NonNull List jobs) { + this.jobManager = jobManager; + this.jobs = new LinkedList<>(); - private Chain(@NonNull List jobs) { this.jobs.add(new ArrayList<>(jobs)); } @@ -141,16 +184,146 @@ public class JobManager { } public Chain then(@NonNull List 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> 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 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 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 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 jobFactories = new HashMap<>(); + private Map constraintFactories = new HashMap<>(); + private List 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 jobFactories) { + this.jobFactories = jobFactories; + return this; + } + + public @NonNull Builder setConstraintFactories(@NonNull Map constraintFactories) { + this.constraintFactories = constraintFactories; + return this; + } + + public @NonNull Builder setConstraintObservers(@NonNull List 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); + } + } + } } diff --git a/src/org/thoughtcrime/securesms/jobmanager/JobParameters.java b/src/org/thoughtcrime/securesms/jobmanager/JobParameters.java deleted file mode 100644 index f45a9b50e..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/JobParameters.java +++ /dev/null @@ -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 . - */ -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 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 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 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)}. - *

- * 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); - } - } -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/JobRunner.java b/src/org/thoughtcrime/securesms/jobmanager/JobRunner.java new file mode 100644 index 000000000..7c67dc339 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/JobRunner.java @@ -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 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)); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/JobSchedulerScheduler.java b/src/org/thoughtcrime/securesms/jobmanager/JobSchedulerScheduler.java new file mode 100644 index 000000000..8e9fe53c4 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/JobSchedulerScheduler.java @@ -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 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; + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/KeepAliveService.java b/src/org/thoughtcrime/securesms/jobmanager/KeepAliveService.java new file mode 100644 index 000000000..ccc1aa234 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/KeepAliveService.java @@ -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; + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/SafeData.java b/src/org/thoughtcrime/securesms/jobmanager/SafeData.java deleted file mode 100644 index 2d7fba3e2..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/SafeData.java +++ /dev/null @@ -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); - } - } -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/Scheduler.java b/src/org/thoughtcrime/securesms/jobmanager/Scheduler.java new file mode 100644 index 000000000..fb276cfe2 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/Scheduler.java @@ -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 constraints); +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/WorkLockManager.java b/src/org/thoughtcrime/securesms/jobmanager/WorkLockManager.java deleted file mode 100644 index d134e072e..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/WorkLockManager.java +++ /dev/null @@ -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 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); - } - } -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/dependencies/ContextDependent.java b/src/org/thoughtcrime/securesms/jobmanager/dependencies/ContextDependent.java deleted file mode 100644 index 84a4aca36..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/dependencies/ContextDependent.java +++ /dev/null @@ -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 . -*/ -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); -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/impl/CellServiceConstraint.java b/src/org/thoughtcrime/securesms/jobmanager/impl/CellServiceConstraint.java new file mode 100644 index 000000000..5fb999736 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/impl/CellServiceConstraint.java @@ -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 { + + private final Application application; + + public Factory(@NonNull Application application) { + this.application = application; + } + + @Override + public CellServiceConstraint create() { + return new CellServiceConstraint(application); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/impl/CellServiceConstraintObserver.java b/src/org/thoughtcrime/securesms/jobmanager/impl/CellServiceConstraintObserver.java new file mode 100644 index 000000000..dfd1b9c71 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/impl/CellServiceConstraintObserver.java @@ -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); + } + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/impl/DefaultExecutorFactory.java b/src/org/thoughtcrime/securesms/jobmanager/impl/DefaultExecutorFactory.java new file mode 100644 index 000000000..91c72474c --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/impl/DefaultExecutorFactory.java @@ -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)); + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/impl/JsonDataSerializer.java b/src/org/thoughtcrime/securesms/jobmanager/impl/JsonDataSerializer.java new file mode 100644 index 000000000..fbabab82f --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/impl/JsonDataSerializer.java @@ -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); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/impl/NetworkConstraint.java b/src/org/thoughtcrime/securesms/jobmanager/impl/NetworkConstraint.java new file mode 100644 index 000000000..1252e4da6 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/impl/NetworkConstraint.java @@ -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 { + + private final Application application; + + public Factory(@NonNull Application application) { + this.application = application; + } + + @Override + public NetworkConstraint create() { + return new NetworkConstraint(application); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/impl/NetworkConstraintObserver.java b/src/org/thoughtcrime/securesms/jobmanager/impl/NetworkConstraintObserver.java new file mode 100644 index 000000000..fd99ae367 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/impl/NetworkConstraintObserver.java @@ -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)); + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/impl/NetworkOrCellServiceConstraint.java b/src/org/thoughtcrime/securesms/jobmanager/impl/NetworkOrCellServiceConstraint.java new file mode 100644 index 000000000..565f69c30 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/impl/NetworkOrCellServiceConstraint.java @@ -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 { + + private final Application application; + + public Factory(@NonNull Application application) { + this.application = application; + } + + @Override + public NetworkOrCellServiceConstraint create() { + return new NetworkOrCellServiceConstraint(application); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/impl/SqlCipherMigrationConstraint.java b/src/org/thoughtcrime/securesms/jobmanager/impl/SqlCipherMigrationConstraint.java new file mode 100644 index 000000000..d3bb5c926 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/impl/SqlCipherMigrationConstraint.java @@ -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 { + + private final Application application; + + public Factory(@NonNull Application application) { + this.application = application; + } + + @Override + public SqlCipherMigrationConstraint create() { + return new SqlCipherMigrationConstraint(application); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/impl/SqlCipherMigrationConstraintObserver.java b/src/org/thoughtcrime/securesms/jobmanager/impl/SqlCipherMigrationConstraintObserver.java new file mode 100644 index 000000000..96b858e9d --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/impl/SqlCipherMigrationConstraintObserver.java @@ -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 { + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/migration/DataMigrator.java b/src/org/thoughtcrime/securesms/jobmanager/migration/DataMigrator.java new file mode 100644 index 000000000..d38dd21fb --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/migration/DataMigrator.java @@ -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 values = parseWorkManagerDataMap(workManagerData); + + Data.Builder builder = new Data.Builder(); + + for (Map.Entry 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 parseWorkManagerDataMap(@NonNull byte[] bytes) throws IllegalStateException { + Map 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; + } +} + diff --git a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerDatabase.java b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerDatabase.java new file mode 100644 index 000000000..2f4696a28 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerDatabase.java @@ -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 getAllJobs(@NonNull Data.Serializer dataSerializer) { + SQLiteDatabase db = getReadableDatabase(); + String[] columns = new String[] { "id", "worker_class_name", "input", "required_network_type"}; + List 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 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; + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java new file mode 100644 index 000000000..156fdf9b0 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java @@ -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 FACTORY_MAP = new HashMap() {{ + 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); + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerMigrator.java b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerMigrator.java new file mode 100644 index 000000000..1bc7fcc96 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerMigrator.java @@ -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 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(); + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/persistence/ConstraintSpec.java b/src/org/thoughtcrime/securesms/jobmanager/persistence/ConstraintSpec.java new file mode 100644 index 000000000..8bec3a703 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/persistence/ConstraintSpec.java @@ -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); + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/persistence/DependencySpec.java b/src/org/thoughtcrime/securesms/jobmanager/persistence/DependencySpec.java new file mode 100644 index 000000000..644c4d122 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/persistence/DependencySpec.java @@ -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); + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/persistence/FullSpec.java b/src/org/thoughtcrime/securesms/jobmanager/persistence/FullSpec.java new file mode 100644 index 000000000..1a70a9eea --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/persistence/FullSpec.java @@ -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 constraintSpecs; + private final List dependencySpecs; + + public FullSpec(@NonNull JobSpec jobSpec, + @NonNull List constraintSpecs, + @NonNull List dependencySpecs) + { + this.jobSpec = jobSpec; + this.constraintSpecs = constraintSpecs; + this.dependencySpecs = dependencySpecs; + } + + public @NonNull JobSpec getJobSpec() { + return jobSpec; + } + + public @NonNull List getConstraintSpecs() { + return constraintSpecs; + } + + public @NonNull List 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); + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/persistence/JavaJobSerializer.java b/src/org/thoughtcrime/securesms/jobmanager/persistence/JavaJobSerializer.java deleted file mode 100644 index 655f655f3..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/persistence/JavaJobSerializer.java +++ /dev/null @@ -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 . - */ -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()); - } - } -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/persistence/JobSerializer.java b/src/org/thoughtcrime/securesms/jobmanager/persistence/JobSerializer.java deleted file mode 100644 index 533f72cc1..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/persistence/JobSerializer.java +++ /dev/null @@ -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 . - */ -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; - -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/persistence/JobSpec.java b/src/org/thoughtcrime/securesms/jobmanager/persistence/JobSpec.java new file mode 100644 index 000000000..e8a72ae45 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/persistence/JobSpec.java @@ -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); + } +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/persistence/JobStorage.java b/src/org/thoughtcrime/securesms/jobmanager/persistence/JobStorage.java new file mode 100644 index 000000000..92d6fd31f --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobmanager/persistence/JobStorage.java @@ -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 fullSpecs); + + @WorkerThread + @Nullable JobSpec getJobSpec(@NonNull String id); + + @WorkerThread + @NonNull List getAllJobSpecs(); + + @WorkerThread + @NonNull List 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 ids); + + @WorkerThread + @NonNull List getConstraintSpecs(@NonNull String jobId); + + @WorkerThread + @NonNull List getAllConstraintSpecs(); + + @WorkerThread + @NonNull List getDependencySpecsThatDependOnJob(@NonNull String jobSpecId); + + @WorkerThread + @NonNull List getAllDependencySpecs(); +} diff --git a/src/org/thoughtcrime/securesms/jobmanager/persistence/PersistentStorage.java b/src/org/thoughtcrime/securesms/jobmanager/persistence/PersistentStorage.java deleted file mode 100644 index 4da766f77..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/persistence/PersistentStorage.java +++ /dev/null @@ -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 . - */ -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 getAllUnencrypted() { - return getJobs(null, ENCRYPTED + " = 0"); - } - - private List getJobs(EncryptionKeys keys, String where) { - List 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) { - - } - } - -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkBackoffRequirement.java b/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkBackoffRequirement.java deleted file mode 100644 index 147f4342b..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkBackoffRequirement.java +++ /dev/null @@ -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; - } -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkRequirement.java b/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkRequirement.java deleted file mode 100644 index 050fac6a2..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkRequirement.java +++ /dev/null @@ -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 . - */ -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; - } -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkRequirementProvider.java b/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkRequirementProvider.java deleted file mode 100644 index 788c09eee..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/requirements/NetworkRequirementProvider.java +++ /dev/null @@ -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 . - */ -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; - } - -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/requirements/Requirement.java b/src/org/thoughtcrime/securesms/jobmanager/requirements/Requirement.java deleted file mode 100644 index 3631efb36..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/requirements/Requirement.java +++ /dev/null @@ -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 . - */ -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); -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/requirements/RequirementListener.java b/src/org/thoughtcrime/securesms/jobmanager/requirements/RequirementListener.java deleted file mode 100644 index a548adfb9..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/requirements/RequirementListener.java +++ /dev/null @@ -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 . - */ -package org.thoughtcrime.securesms.jobmanager.requirements; - -public interface RequirementListener { - public void onRequirementStatusChanged(); -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/requirements/RequirementProvider.java b/src/org/thoughtcrime/securesms/jobmanager/requirements/RequirementProvider.java deleted file mode 100644 index 95e1c19af..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/requirements/RequirementProvider.java +++ /dev/null @@ -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 . - */ -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); -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/requirements/SimpleRequirement.java b/src/org/thoughtcrime/securesms/jobmanager/requirements/SimpleRequirement.java deleted file mode 100644 index e6e852b00..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/requirements/SimpleRequirement.java +++ /dev/null @@ -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(); -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/util/Base64.java b/src/org/thoughtcrime/securesms/jobmanager/util/Base64.java deleted file mode 100644 index f44b21deb..000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/util/Base64.java +++ /dev/null @@ -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 2045 and 3548. - */ -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. - * - *

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. - * - *

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. - * - *

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 - * RFC 2045). - */ - 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 -} diff --git a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java index bc95038fc..6cda006c9 100644 --- a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -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 { + @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)); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java b/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java index 2e758f34b..c9866c390 100644 --- a/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java @@ -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 { + @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))); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java index 2f1af5d21..21ae4b3ab 100644 --- a/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java @@ -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 { + @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); + } + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/BaseJob.java b/src/org/thoughtcrime/securesms/jobs/BaseJob.java new file mode 100644 index 000000000..9d4e76b03 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/BaseJob.java @@ -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); +} diff --git a/src/org/thoughtcrime/securesms/jobs/CleanPreKeysJob.java b/src/org/thoughtcrime/securesms/jobs/CleanPreKeysJob.java index c83377f39..d239a9d7c 100644 --- a/src/org/thoughtcrime/securesms/jobs/CleanPreKeysJob.java +++ b/src/org/thoughtcrime/securesms/jobs/CleanPreKeysJob.java @@ -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 { + @Override + public @NonNull CleanPreKeysJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new CleanPreKeysJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/ContextJob.java b/src/org/thoughtcrime/securesms/jobs/ContextJob.java deleted file mode 100644 index d9f38e088..000000000 --- a/src/org/thoughtcrime/securesms/jobs/ContextJob.java +++ /dev/null @@ -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; - } -} diff --git a/src/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java b/src/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java index c4e7f28db..cfb5a9fdf 100644 --- a/src/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java +++ b/src/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java @@ -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 { + @Override + public @NonNull CreateSignedPreKeyJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new CreateSignedPreKeyJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java b/src/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java index 9fa70d988..a7074187f 100644 --- a/src/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java +++ b/src/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java @@ -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 { + + 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); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/FastJobStorage.java b/src/org/thoughtcrime/securesms/jobs/FastJobStorage.java new file mode 100644 index 000000000..90a3243ae --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/FastJobStorage.java @@ -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 jobs; + private final Map> constraintsByJobId; + private final Map> 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 jobSpecs = jobDatabase.getAllJobSpecs(); + List constraintSpecs = jobDatabase.getAllConstraintSpecs(); + List dependencySpecs = jobDatabase.getAllDependencySpecs(); + + jobs.addAll(jobSpecs); + + for (ConstraintSpec constraintSpec: constraintSpecs) { + List jobConstraints = Util.getOrDefault(constraintsByJobId, constraintSpec.getJobSpecId(), new LinkedList<>()); + jobConstraints.add(constraintSpec); + constraintsByJobId.put(constraintSpec.getJobSpecId(), jobConstraints); + } + + for (DependencySpec dependencySpec : dependencySpecs) { + List jobDependencies = Util.getOrDefault(dependenciesByJobId, dependencySpec.getJobId(), new LinkedList<>()); + jobDependencies.add(dependencySpec); + dependenciesByJobId.put(dependencySpec.getJobId(), jobDependencies); + } + } + + @Override + public synchronized void insertJobs(@NonNull List 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 getAllJobSpecs() { + return new ArrayList<>(jobs); + } + + @Override + public synchronized @NonNull List 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 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 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 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 jobIds) { + jobDatabase.deleteJobs(jobIds); + + Set deleteIds = new HashSet<>(jobIds); + + Iterator 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> entry : dependenciesByJobId.entrySet()) { + Iterator depedencyIter = entry.getValue().iterator(); + + while (depedencyIter.hasNext()) { + if (depedencyIter.next().getDependsOnJobId().equals(jobId)) { + depedencyIter.remove(); + } + } + } + } + } + + @Override + public synchronized @NonNull List getConstraintSpecs(@NonNull String jobId) { + return Util.getOrDefault(constraintsByJobId, jobId, new LinkedList<>()); + } + + @Override + public synchronized @NonNull List getAllConstraintSpecs() { + return Stream.of(constraintsByJobId) + .map(Map.Entry::getValue) + .flatMap(Stream::of) + .toList(); + } + + @Override + public synchronized @NonNull List 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 getAllDependencySpecs() { + return Stream.of(dependenciesByJobId) + .map(Map.Entry::getValue) + .flatMap(Stream::of) + .toList(); + } +} diff --git a/src/org/thoughtcrime/securesms/jobs/FcmRefreshJob.java b/src/org/thoughtcrime/securesms/jobs/FcmRefreshJob.java index 8a16b4acb..245a222ea 100644 --- a/src/org/thoughtcrime/securesms/jobs/FcmRefreshJob.java +++ b/src/org/thoughtcrime/securesms/jobs/FcmRefreshJob.java @@ -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 { + @Override + public @NonNull FcmRefreshJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new FcmRefreshJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java new file mode 100644 index 000000000..8a5912cf8 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -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 getJobFactories(@NonNull Application application) { + return new HashMap() {{ + 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 getConstraintFactories(@NonNull Application application) { + return new HashMap() {{ + 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 getConstraintObservers(@NonNull Application application) { + return Arrays.asList(new CellServiceConstraintObserver(application), + new NetworkConstraintObserver(application), + new SqlCipherMigrationConstraintObserver()); + } +} diff --git a/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java b/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java index a3b956ba3..4fe6ecf61 100644 --- a/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java +++ b/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java @@ -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 { + @Override + public @NonNull LocalBackupJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new LocalBackupJob(parameters); + } } } diff --git a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index 603f25026..e418d416a 100644 --- a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -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 { + @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)); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/MmsReceiveJob.java b/src/org/thoughtcrime/securesms/jobs/MmsReceiveJob.java index 7518e3e05..d445ba568 100644 --- a/src/org/thoughtcrime/securesms/jobs/MmsReceiveJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MmsReceiveJob.java @@ -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 { + @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); + } + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java b/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java index aa9fc8be1..bacda8b02 100644 --- a/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java @@ -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 { + @Override + public @NonNull MmsSendJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new MmsSendJob(parameters, data.getLong(KEY_MESSAGE_ID)); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceBlockedUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceBlockedUpdateJob.java index 81e283135..f575a63b7 100644 --- a/src/org/thoughtcrime/securesms/jobs/MultiDeviceBlockedUpdateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceBlockedUpdateJob.java @@ -1,16 +1,15 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; -import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientReader; 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.recipients.Recipient; import org.thoughtcrime.securesms.util.GroupUtil; @@ -24,39 +23,40 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import java.io.IOException; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class MultiDeviceBlockedUpdateJob extends BaseJob implements InjectableType { -public class MultiDeviceBlockedUpdateJob extends ContextJob implements InjectableType { - - private static final long serialVersionUID = 1L; + public static final String KEY = "MultiDeviceBlockedUpdateJob"; @SuppressWarnings("unused") private static final String TAG = MultiDeviceBlockedUpdateJob.class.getSimpleName(); - @Inject transient SignalServiceMessageSender messageSender; + @Inject SignalServiceMessageSender messageSender; - public MultiDeviceBlockedUpdateJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public MultiDeviceBlockedUpdateJob() { + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setQueue("MultiDeviceBlockedUpdateJob") + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build()); } - public MultiDeviceBlockedUpdateJob(Context context) { - super(context, JobParameters.newBuilder() - .withNetworkRequirement() - .withGroupId(MultiDeviceBlockedUpdateJob.class.getSimpleName()) - .create()); + private MultiDeviceBlockedUpdateJob(@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 @@ -97,6 +97,12 @@ public class MultiDeviceBlockedUpdateJob extends ContextJob implements Injectabl @Override public void onCanceled() { + } + public static final class Factory implements Job.Factory { + @Override + public @NonNull MultiDeviceBlockedUpdateJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new MultiDeviceBlockedUpdateJob(parameters); + } } } diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceConfigurationUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceConfigurationUpdateJob.java index fe09ad61f..b96081557 100644 --- a/src/org/thoughtcrime/securesms/jobs/MultiDeviceConfigurationUpdateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceConfigurationUpdateJob.java @@ -1,13 +1,13 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; 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.util.guava.Optional; @@ -21,12 +21,9 @@ import java.io.IOException; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class MultiDeviceConfigurationUpdateJob extends BaseJob implements InjectableType { -public class MultiDeviceConfigurationUpdateJob extends ContextJob implements InjectableType { - - private static final long serialVersionUID = 1L; + public static final String KEY = "MultiDeviceConfigurationUpdateJob"; private static final String TAG = MultiDeviceConfigurationUpdateJob.class.getSimpleName(); @@ -35,27 +32,37 @@ public class MultiDeviceConfigurationUpdateJob extends ContextJob implements Inj private static final String KEY_UNIDENTIFIED_DELIVERY_INDICATORS_ENABLED = "unidentified_delivery_indicators_enabled"; private static final String KEY_LINK_PREVIEWS_ENABLED = "link_previews_enabled"; - @Inject transient SignalServiceMessageSender messageSender; + @Inject SignalServiceMessageSender messageSender; private boolean readReceiptsEnabled; private boolean typingIndicatorsEnabled; private boolean unidentifiedDeliveryIndicatorsEnabled; private boolean linkPreviewsEnabled; - public MultiDeviceConfigurationUpdateJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); - } - - public MultiDeviceConfigurationUpdateJob(Context context, - boolean readReceiptsEnabled, + public MultiDeviceConfigurationUpdateJob(boolean readReceiptsEnabled, boolean typingIndicatorsEnabled, boolean unidentifiedDeliveryIndicatorsEnabled, boolean linkPreviewsEnabled) { - super(context, JobParameters.newBuilder() - .withGroupId("__MULTI_DEVICE_CONFIGURATION_UPDATE_JOB__") - .withNetworkRequirement() - .create()); + this(new Job.Parameters.Builder() + .setQueue("__MULTI_DEVICE_CONFIGURATION_UPDATE_JOB__") + .addConstraint(NetworkConstraint.KEY) + .setMaxAttempts(10) + .build(), + readReceiptsEnabled, + typingIndicatorsEnabled, + unidentifiedDeliveryIndicatorsEnabled, + linkPreviewsEnabled); + + } + + private MultiDeviceConfigurationUpdateJob(@NonNull Job.Parameters parameters, + boolean readReceiptsEnabled, + boolean typingIndicatorsEnabled, + boolean unidentifiedDeliveryIndicatorsEnabled, + boolean linkPreviewsEnabled) + { + super(parameters); this.readReceiptsEnabled = readReceiptsEnabled; this.typingIndicatorsEnabled = typingIndicatorsEnabled; @@ -64,20 +71,17 @@ public class MultiDeviceConfigurationUpdateJob extends ContextJob implements Inj } @Override - protected void initialize(@NonNull SafeData data) { - readReceiptsEnabled = data.getBoolean(KEY_READ_RECEIPTS_ENABLED); - typingIndicatorsEnabled = data.getBoolean(KEY_TYPING_INDICATORS_ENABLED); - unidentifiedDeliveryIndicatorsEnabled = data.getBoolean(KEY_UNIDENTIFIED_DELIVERY_INDICATORS_ENABLED); - linkPreviewsEnabled = data.getBoolean(KEY_LINK_PREVIEWS_ENABLED); + public @NonNull Data serialize() { + return new Data.Builder().putBoolean(KEY_READ_RECEIPTS_ENABLED, readReceiptsEnabled) + .putBoolean(KEY_TYPING_INDICATORS_ENABLED, typingIndicatorsEnabled) + .putBoolean(KEY_UNIDENTIFIED_DELIVERY_INDICATORS_ENABLED, unidentifiedDeliveryIndicatorsEnabled) + .putBoolean(KEY_LINK_PREVIEWS_ENABLED, linkPreviewsEnabled) + .build(); } @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putBoolean(KEY_READ_RECEIPTS_ENABLED, readReceiptsEnabled) - .putBoolean(KEY_TYPING_INDICATORS_ENABLED, typingIndicatorsEnabled) - .putBoolean(KEY_UNIDENTIFIED_DELIVERY_INDICATORS_ENABLED, unidentifiedDeliveryIndicatorsEnabled) - .putBoolean(KEY_LINK_PREVIEWS_ENABLED, linkPreviewsEnabled) - .build(); + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -103,4 +107,15 @@ public class MultiDeviceConfigurationUpdateJob extends ContextJob implements Inj public void onCanceled() { Log.w(TAG, "**** Failed to synchronize read receipts state!"); } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull MultiDeviceConfigurationUpdateJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new MultiDeviceConfigurationUpdateJob(parameters, + data.getBooleanOrDefault(KEY_READ_RECEIPTS_ENABLED, false), + data.getBooleanOrDefault(KEY_TYPING_INDICATORS_ENABLED, false), + data.getBooleanOrDefault(KEY_UNIDENTIFIED_DELIVERY_INDICATORS_ENABLED, false), + data.getBooleanOrDefault(KEY_LINK_PREVIEWS_ENABLED, false)); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java index 2ed85cfab..d39bd6825 100644 --- a/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java @@ -5,7 +5,6 @@ import android.content.Context; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.net.Uri; -import android.os.Build; import android.provider.ContactsContract; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -19,8 +18,9 @@ import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.IdentityDatabase; 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.permissions.Permissions; import org.thoughtcrime.securesms.recipients.Recipient; @@ -49,12 +49,9 @@ import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableType { -public class MultiDeviceContactUpdateJob extends ContextJob implements InjectableType { - - private static final long serialVersionUID = 2L; + public static final String KEY = "MultiDeviceContactUpdateJob"; private static final String TAG = MultiDeviceContactUpdateJob.class.getSimpleName(); @@ -63,16 +60,12 @@ public class MultiDeviceContactUpdateJob extends ContextJob implements Injectabl private static final String KEY_ADDRESS = "address"; private static final String KEY_FORCE_SYNC = "force_sync"; - @Inject transient SignalServiceMessageSender messageSender; + @Inject SignalServiceMessageSender messageSender; private @Nullable String address; private boolean forceSync; - public MultiDeviceContactUpdateJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); - } - public MultiDeviceContactUpdateJob(@NonNull Context context) { this(context, false); } @@ -86,10 +79,18 @@ public class MultiDeviceContactUpdateJob extends ContextJob implements Injectabl } public MultiDeviceContactUpdateJob(@NonNull Context context, @Nullable Address address, boolean forceSync) { - super(context, JobParameters.newBuilder() - .withNetworkRequirement() - .withGroupId(MultiDeviceContactUpdateJob.class.getSimpleName()) - .create()); + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setQueue("MultiDeviceContactUpdateJob") + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + address, + forceSync); + } + + private MultiDeviceContactUpdateJob(@NonNull Job.Parameters parameters, @Nullable Address address, boolean forceSync) { + super(parameters); this.forceSync = forceSync; @@ -98,16 +99,15 @@ public class MultiDeviceContactUpdateJob extends ContextJob implements Injectabl } @Override - protected void initialize(@NonNull SafeData data) { - address = data.getString(KEY_ADDRESS); - forceSync = data.getBoolean(KEY_FORCE_SYNC); + public @NonNull Data serialize() { + return new Data.Builder().putString(KEY_ADDRESS, address) + .putBoolean(KEY_FORCE_SYNC, forceSync) + .build(); } @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putString(KEY_ADDRESS, address) - .putBoolean(KEY_FORCE_SYNC, forceSync) - .build(); + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -335,4 +335,13 @@ public class MultiDeviceContactUpdateJob extends ContextJob implements Injectabl } } + public static final class Factory implements Job.Factory { + @Override + public @NonNull MultiDeviceContactUpdateJob create(@NonNull Parameters parameters, @NonNull Data data) { + String serialized = data.getString(KEY_ADDRESS); + Address address = serialized != null ? Address.fromSerialized(serialized) : null; + + return new MultiDeviceContactUpdateJob(parameters, address, data.getBoolean(KEY_FORCE_SYNC)); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java index 22953b7ee..3611ef6c5 100644 --- a/src/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java @@ -1,20 +1,19 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.dependencies.InjectableType; -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.recipients.Recipient; import org.thoughtcrime.securesms.util.GroupUtil; -import org.thoughtcrime.securesms.jobmanager.JobParameters; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; @@ -33,37 +32,39 @@ import java.io.FileOutputStream; import java.io.IOException; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class MultiDeviceGroupUpdateJob extends BaseJob implements InjectableType { -public class MultiDeviceGroupUpdateJob extends ContextJob implements InjectableType { + public static final String KEY = "MultiDeviceGroupUpdateJob"; - private static final long serialVersionUID = 1L; private static final String TAG = MultiDeviceGroupUpdateJob.class.getSimpleName(); - @Inject transient SignalServiceMessageSender messageSender; + @Inject SignalServiceMessageSender messageSender; - public MultiDeviceGroupUpdateJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public MultiDeviceGroupUpdateJob() { + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setQueue("MultiDeviceGroupUpdateJob") + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build()); } - public MultiDeviceGroupUpdateJob(Context context) { - super(context, JobParameters.newBuilder() - .withNetworkRequirement() - .withGroupId(MultiDeviceGroupUpdateJob.class.getSimpleName()) - .create()); + private MultiDeviceGroupUpdateJob(@NonNull Job.Parameters parameters) { + super(parameters); } @Override - protected void initialize(@NonNull SafeData data) { + public @NonNull String getFactoryKey() { + return KEY; } @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.build(); + public @NonNull Data serialize() { + return Data.EMPTY; } @Override @@ -160,5 +161,10 @@ public class MultiDeviceGroupUpdateJob extends ContextJob implements InjectableT return file; } - + public static final class Factory implements Job.Factory { + @Override + public @NonNull MultiDeviceGroupUpdateJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new MultiDeviceGroupUpdateJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java index f9782d60f..593479a2a 100644 --- a/src/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java @@ -1,17 +1,16 @@ 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.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.JobParameters; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; @@ -27,51 +26,53 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class MultiDeviceProfileKeyUpdateJob extends BaseJob implements InjectableType { -public class MultiDeviceProfileKeyUpdateJob extends ContextJob implements InjectableType { + public static String KEY = "MultiDeviceProfileKeyUpdateJob"; - private static final long serialVersionUID = 1L; private static final String TAG = MultiDeviceProfileKeyUpdateJob.class.getSimpleName(); - @Inject transient SignalServiceMessageSender messageSender; + @Inject SignalServiceMessageSender messageSender; - public MultiDeviceProfileKeyUpdateJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public MultiDeviceProfileKeyUpdateJob() { + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setQueue("MultiDeviceProfileKeyUpdateJob") + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build()); } - public MultiDeviceProfileKeyUpdateJob(Context context) { - super(context, JobParameters.newBuilder() - .withNetworkRequirement() - .withGroupId(MultiDeviceProfileKeyUpdateJob.class.getSimpleName()) - .create()); + private MultiDeviceProfileKeyUpdateJob(@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 public void onRun() throws IOException, UntrustedIdentityException { - if (!TextSecurePreferences.isMultiDevice(getContext())) { + if (!TextSecurePreferences.isMultiDevice(context)) { Log.i(TAG, "Not multi device..."); return; } - Optional profileKey = Optional.of(ProfileKeyUtil.getProfileKey(getContext())); + Optional profileKey = Optional.of(ProfileKeyUtil.getProfileKey(context)); ByteArrayOutputStream baos = new ByteArrayOutputStream(); DeviceContactsOutputStream out = new DeviceContactsOutputStream(baos); - out.write(new DeviceContact(TextSecurePreferences.getLocalNumber(getContext()), + out.write(new DeviceContact(TextSecurePreferences.getLocalNumber(context), Optional.absent(), Optional.absent(), Optional.absent(), @@ -101,4 +102,11 @@ public class MultiDeviceProfileKeyUpdateJob extends ContextJob implements Inject public void onCanceled() { Log.w(TAG, "Profile key sync failed!"); } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull MultiDeviceProfileKeyUpdateJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new MultiDeviceProfileKeyUpdateJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceReadReceiptUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceReadReceiptUpdateJob.java deleted file mode 100644 index dac32d10f..000000000 --- a/src/org/thoughtcrime/securesms/jobs/MultiDeviceReadReceiptUpdateJob.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - - -import android.content.Context; -import android.support.annotation.NonNull; - -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; -import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.JobParameters; -import org.thoughtcrime.securesms.jobmanager.SafeData; -import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.SignalServiceMessageSender; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; -import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; - -import java.io.IOException; - -import javax.inject.Inject; - -import androidx.work.Data; -import androidx.work.WorkerParameters; - -/** - * Use {@link MultiDeviceConfigurationUpdateJob}. - */ -@Deprecated -public class MultiDeviceReadReceiptUpdateJob extends ContextJob implements InjectableType { - - private static final long serialVersionUID = 1L; - - private static final String TAG = MultiDeviceReadReceiptUpdateJob.class.getSimpleName(); - - private static final String KEY_ENABLED = "enabled"; - - @Inject transient SignalServiceMessageSender messageSender; - - private boolean enabled; - - public MultiDeviceReadReceiptUpdateJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); - } - - public MultiDeviceReadReceiptUpdateJob(Context context, boolean enabled) { - super(context, JobParameters.newBuilder() - .withGroupId("__MULTI_DEVICE_READ_RECEIPT_UPDATE_JOB__") - .withNetworkRequirement() - .create()); - - this.enabled = enabled; - } - - @Override - protected void initialize(@NonNull SafeData data) { - enabled = data.getBoolean(KEY_ENABLED); - } - - @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putBoolean(KEY_ENABLED, enabled).build(); - } - - @Override - public void onRun() throws IOException, UntrustedIdentityException { - if (!TextSecurePreferences.isMultiDevice(context)) { - Log.i(TAG, "Not multi device, aborting..."); - return; - } - - messageSender.sendMessage(SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.of(enabled), Optional.absent(), Optional.absent(), Optional.absent())), - UnidentifiedAccessUtil.getAccessForSync(context)); - } - - @Override - public boolean onShouldRetry(Exception e) { - return e instanceof PushNetworkException; - } - - @Override - public void onCanceled() { - Log.w(TAG, "**** Failed to synchronize read receipts state!"); - } -} diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceReadUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceReadUpdateJob.java index 9df30509c..82e269c89 100644 --- a/src/org/thoughtcrime/securesms/jobs/MultiDeviceReadUpdateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceReadUpdateJob.java @@ -1,21 +1,21 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; +import com.annimon.stream.Stream; import com.fasterxml.jackson.annotation.JsonProperty; -import org.thoughtcrime.securesms.jobmanager.SafeData; +import org.thoughtcrime.securesms.database.Address; +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.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.JobParameters; import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; @@ -24,34 +24,35 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import java.io.IOException; import java.io.Serializable; -import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class MultiDeviceReadUpdateJob extends BaseJob implements InjectableType { -public class MultiDeviceReadUpdateJob extends ContextJob implements InjectableType { + public static final String KEY = "MultiDeviceReadUpdateJob"; - private static final long serialVersionUID = 1L; private static final String TAG = MultiDeviceReadUpdateJob.class.getSimpleName(); private static final String KEY_MESSAGE_IDS = "message_ids"; private List messageIds; - @Inject transient SignalServiceMessageSender messageSender; + @Inject SignalServiceMessageSender messageSender; - public MultiDeviceReadUpdateJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public MultiDeviceReadUpdateJob(List messageIds) { + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + messageIds); } - public MultiDeviceReadUpdateJob(Context context, List messageIds) { - super(context, JobParameters.newBuilder() - .withNetworkRequirement() - .create()); + private MultiDeviceReadUpdateJob(@NonNull Job.Parameters parameters, @NonNull List messageIds) { + super(parameters); this.messageIds = new LinkedList<>(); @@ -61,21 +62,7 @@ public class MultiDeviceReadUpdateJob extends ContextJob implements InjectableTy } @Override - protected void initialize(@NonNull SafeData data) { - String[] ids = data.getStringArray(KEY_MESSAGE_IDS); - - messageIds = new ArrayList<>(ids.length); - for (String id : ids) { - try { - messageIds.add(JsonUtils.fromJson(id, SerializableSyncMessageId.class)); - } catch (IOException e) { - throw new AssertionError(e); - } - } - } - - @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { + public @NonNull Data serialize() { String[] ids = new String[messageIds.size()]; for (int i = 0; i < ids.length; i++) { @@ -86,7 +73,12 @@ public class MultiDeviceReadUpdateJob extends ContextJob implements InjectableTy } } - return dataBuilder.putStringArray(KEY_MESSAGE_IDS, ids).build(); + return new Data.Builder().putStringArray(KEY_MESSAGE_IDS, ids).build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -130,4 +122,23 @@ public class MultiDeviceReadUpdateJob extends ContextJob implements InjectableTy this.timestamp = timestamp; } } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull MultiDeviceReadUpdateJob create(@NonNull Parameters parameters, @NonNull Data data) { + List ids = Stream.of(data.getStringArray(KEY_MESSAGE_IDS)) + .map(id -> { + try { + return JsonUtils.fromJson(id, SerializableSyncMessageId.class); + } catch (IOException e) { + throw new AssertionError(e); + } + }) + .map(id -> new SyncMessageId(Address.fromSerialized(id.sender), id.timestamp)) + .toList(); + + return new MultiDeviceReadUpdateJob(parameters, ids); + + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java index 862a117b3..a23fa062f 100644 --- a/src/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java @@ -1,19 +1,18 @@ 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.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus; import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.JobParameters; import org.thoughtcrime.securesms.util.Base64; -import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.IdentityKey; @@ -25,15 +24,13 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import java.io.IOException; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class MultiDeviceVerifiedUpdateJob extends BaseJob implements InjectableType { -public class MultiDeviceVerifiedUpdateJob extends ContextJob implements InjectableType { - - private static final long serialVersionUID = 1L; + public static final String KEY = "MultiDeviceVerifiedUpdateJob"; private static final String TAG = MultiDeviceVerifiedUpdateJob.class.getSimpleName(); @@ -42,50 +39,52 @@ public class MultiDeviceVerifiedUpdateJob extends ContextJob implements Injectab private static final String KEY_VERIFIED_STATUS = "verified_status"; private static final String KEY_TIMESTAMP = "timestamp"; - @Inject - transient SignalServiceMessageSender messageSender; + @Inject SignalServiceMessageSender messageSender; private String destination; private byte[] identityKey; private VerifiedStatus verifiedStatus; private long timestamp; - public MultiDeviceVerifiedUpdateJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public MultiDeviceVerifiedUpdateJob(Address destination, IdentityKey identityKey, VerifiedStatus verifiedStatus) { + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setQueue("__MULTI_DEVICE_VERIFIED_UPDATE__") + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + destination, + identityKey.serialize(), + verifiedStatus, + System.currentTimeMillis()); } - public MultiDeviceVerifiedUpdateJob(Context context, Address destination, IdentityKey identityKey, VerifiedStatus verifiedStatus) { - super(context, JobParameters.newBuilder() - .withNetworkRequirement() - .withGroupId("__MULTI_DEVICE_VERIFIED_UPDATE__") - .create()); + private MultiDeviceVerifiedUpdateJob(@NonNull Job.Parameters parameters, + @NonNull Address destination, + @NonNull byte[] identityKey, + @NonNull VerifiedStatus verifiedStatus, + long timestamp) + { + super(parameters); this.destination = destination.serialize(); - this.identityKey = identityKey.serialize(); + this.identityKey = identityKey; this.verifiedStatus = verifiedStatus; - this.timestamp = System.currentTimeMillis(); + this.timestamp = timestamp; } @Override - protected void initialize(@NonNull SafeData data) { - destination = data.getString(KEY_DESTINATION); - verifiedStatus = VerifiedStatus.forState(data.getInt(KEY_VERIFIED_STATUS)); - timestamp = data.getLong(KEY_TIMESTAMP); - - try { - identityKey = Base64.decode(data.getString(KEY_IDENTITY_KEY)); - } catch (IOException e) { - throw new AssertionError(e); - } + public @NonNull Data serialize() { + return new Data.Builder().putString(KEY_DESTINATION, destination) + .putString(KEY_IDENTITY_KEY, Base64.encodeBytes(identityKey)) + .putInt(KEY_VERIFIED_STATUS, verifiedStatus.toInt()) + .putLong(KEY_TIMESTAMP, timestamp) + .build(); } @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putString(KEY_DESTINATION, destination) - .putString(KEY_IDENTITY_KEY, Base64.encodeBytes(identityKey)) - .putInt(KEY_VERIFIED_STATUS, verifiedStatus.toInt()) - .putLong(KEY_TIMESTAMP, timestamp) - .build(); + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -134,4 +133,20 @@ public class MultiDeviceVerifiedUpdateJob extends ContextJob implements Injectab public void onCanceled() { } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull MultiDeviceVerifiedUpdateJob create(@NonNull Parameters parameters, @NonNull Data data) { + try { + Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION)); + VerifiedStatus verifiedStatus = VerifiedStatus.forState(data.getInt(KEY_VERIFIED_STATUS)); + long timestamp = data.getLong(KEY_TIMESTAMP); + byte[] identityKey = Base64.decode(data.getString(KEY_IDENTITY_KEY)); + + return new MultiDeviceVerifiedUpdateJob(parameters, destination, identityKey, verifiedStatus, timestamp); + } catch (IOException e) { + throw new AssertionError(e); + } + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java b/src/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java index 55b7afac4..060f36c10 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java @@ -3,37 +3,30 @@ package org.thoughtcrime.securesms.jobs; import android.content.Context; import android.support.annotation.NonNull; -import org.thoughtcrime.securesms.jobmanager.SafeData; -import org.thoughtcrime.securesms.logging.Log; - -import org.thoughtcrime.securesms.jobmanager.JobParameters; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.libsignal.InvalidVersionException; -import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; - -import java.io.IOException; - -import androidx.work.Data; -import androidx.work.WorkerParameters; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; public class PushContentReceiveJob extends PushReceivedJob { - private static final long serialVersionUID = 5685475456901715638L; - - public PushContentReceiveJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); - } + public static final String KEY = "PushContentReceiveJob"; public PushContentReceiveJob(Context context) { - super(context, JobParameters.newBuilder().create()); + this(new Job.Parameters.Builder().build()); + setContext(context); + } + + private PushContentReceiveJob(@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 @@ -46,4 +39,11 @@ public class PushContentReceiveJob extends PushReceivedJob { public boolean onShouldRetry(Exception exception) { return false; } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull PushContentReceiveJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new PushContentReceiveJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index a33d07121..2ac05bc40 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -55,8 +55,8 @@ import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.groups.GroupMessageProcessor; -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.linkpreview.Link; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; @@ -112,12 +112,9 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class PushDecryptJob extends BaseJob { -public class PushDecryptJob extends ContextJob { - - private static final long serialVersionUID = 2L; + public static final String KEY = "PushDecryptJob"; public static final String TAG = PushDecryptJob.class.getSimpleName(); @@ -127,10 +124,6 @@ public class PushDecryptJob extends ContextJob { private long messageId; private long smsMessageId; - public PushDecryptJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); - } - public PushDecryptJob(Context context) { this(context, -1); } @@ -140,24 +133,32 @@ public class PushDecryptJob extends ContextJob { } public PushDecryptJob(Context context, long pushMessageId, long smsMessageId) { - super(context, JobParameters.newBuilder() - .withGroupId("__PUSH_DECRYPT_JOB__") - .create()); + this(new Job.Parameters.Builder() + .setQueue("__PUSH_DECRYPT_JOB__") + .setMaxAttempts(10) + .build(), + pushMessageId, + smsMessageId); + setContext(context); + } + + private PushDecryptJob(@NonNull Job.Parameters parameters, long pushMessageId, long smsMessageId) { + super(parameters); + this.messageId = pushMessageId; this.smsMessageId = smsMessageId; } @Override - protected void initialize(@NonNull SafeData data) { - messageId = data.getLong(KEY_MESSAGE_ID); - smsMessageId = data.getLong(KEY_SMS_MESSAGE_ID); + public @NonNull Data serialize() { + return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId) + .putLong(KEY_SMS_MESSAGE_ID, smsMessageId) + .build(); } @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putLong(KEY_MESSAGE_ID, messageId) - .putLong(KEY_SMS_MESSAGE_ID, smsMessageId) - .build(); + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -287,7 +288,7 @@ public class PushDecryptJob extends ContextJob { resetRecipientToPush(Recipient.from(context, Address.fromExternal(context, content.getSender()), false)); if (envelope.isPreKeySignalMessage()) { - ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob(context)); + ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob()); } } catch (ProtocolInvalidVersionException e) { Log.w(TAG, e); @@ -475,7 +476,7 @@ public class PushDecryptJob extends ContextJob { { ApplicationContext.getInstance(context) .getJobManager() - .add(new RequestGroupInfoJob(context, content.getSender(), group.getGroupId())); + .add(new RequestGroupInfoJob(content.getSender(), group.getGroupId())); } private void handleExpirationUpdate(@NonNull SignalServiceContent content, @@ -552,8 +553,8 @@ public class PushDecryptJob extends ContextJob { } if (threadId != null) { - DatabaseFactory.getThreadDatabase(getContext()).setRead(threadId, true); - MessageNotifier.updateNotification(getContext()); + DatabaseFactory.getThreadDatabase(context).setRead(threadId, true); + MessageNotifier.updateNotification(context); } MessageNotifier.setLastDesktopActivityTimestamp(message.getTimestamp()); @@ -567,33 +568,32 @@ public class PushDecryptJob extends ContextJob { if (message.isContactsRequest()) { ApplicationContext.getInstance(context) .getJobManager() - .add(new MultiDeviceContactUpdateJob(getContext(), true)); + .add(new MultiDeviceContactUpdateJob(context, true)); ApplicationContext.getInstance(context) .getJobManager() - .add(new RefreshUnidentifiedDeliveryAbilityJob(context)); + .add(new RefreshUnidentifiedDeliveryAbilityJob()); } if (message.isGroupsRequest()) { ApplicationContext.getInstance(context) .getJobManager() - .add(new MultiDeviceGroupUpdateJob(getContext())); + .add(new MultiDeviceGroupUpdateJob()); } if (message.isBlockedListRequest()) { ApplicationContext.getInstance(context) .getJobManager() - .add(new MultiDeviceBlockedUpdateJob(getContext())); + .add(new MultiDeviceBlockedUpdateJob()); } if (message.isConfigurationRequest()) { ApplicationContext.getInstance(context) .getJobManager() - .add(new MultiDeviceConfigurationUpdateJob(getContext(), - TextSecurePreferences.isReadReceiptsEnabled(getContext()), - TextSecurePreferences.isTypingIndicatorsEnabled(getContext()), - TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()), - TextSecurePreferences.isLinkPreviewsEnabled(getContext()))); + .add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(context), + TextSecurePreferences.isTypingIndicatorsEnabled(context), + TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context), + TextSecurePreferences.isLinkPreviewsEnabled(context))); } } @@ -652,7 +652,7 @@ public class PushDecryptJob extends ContextJob { for (DatabaseAttachment attachment : attachments) { ApplicationContext.getInstance(context) .getJobManager() - .add(new AttachmentDownloadJob(context, insertResult.get().getMessageId(), attachment.getAttachmentId(), false)); + .add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false)); } if (smsMessageId.isPresent()) { @@ -725,7 +725,7 @@ public class PushDecryptJob extends ContextJob { for (DatabaseAttachment attachment : DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageId)) { ApplicationContext.getInstance(context) .getJobManager() - .add(new AttachmentDownloadJob(context, messageId, attachment.getAttachmentId(), false)); + .add(new AttachmentDownloadJob(messageId, attachment.getAttachmentId(), false)); } if (message.getMessage().getExpiresInSeconds() > 0) { @@ -938,7 +938,7 @@ public class PushDecryptJob extends ContextJob { if (recipient.getProfileKey() == null || !MessageDigest.isEqual(recipient.getProfileKey(), message.getProfileKey().get())) { database.setProfileKey(recipient, message.getProfileKey().get()); database.setUnidentifiedAccessMode(recipient, RecipientDatabase.UnidentifiedAccessMode.UNKNOWN); - ApplicationContext.getInstance(context).getJobManager().add(new RetrieveProfileJob(context, recipient)); + ApplicationContext.getInstance(context).getJobManager().add(new RetrieveProfileJob(recipient)); } } @@ -947,7 +947,7 @@ public class PushDecryptJob extends ContextJob { { ApplicationContext.getInstance(context) .getJobManager() - .add(new SendDeliveryReceiptJob(context, Address.fromExternal(context, content.getSender()), message.getTimestamp())); + .add(new SendDeliveryReceiptJob(Address.fromExternal(context, content.getSender()), message.getTimestamp())); } @SuppressLint("DefaultLocale") @@ -1191,4 +1191,10 @@ public class PushDecryptJob extends ContextJob { } } + public static final class Factory implements Job.Factory { + @Override + public @NonNull PushDecryptJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new PushDecryptJob(parameters, data.getLong(KEY_MESSAGE_ID), data.getLong(KEY_SMS_MESSAGE_ID)); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index ba24f4467..6c2fa39fb 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -20,17 +20,15 @@ import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.ChainParameters; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.thoughtcrime.securesms.jobmanager.JobParameters; -import org.thoughtcrime.securesms.jobmanager.SafeData; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.util.GroupUtil; @@ -46,7 +44,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Qu import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; import java.io.IOException; @@ -58,38 +55,36 @@ import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; - public class PushGroupSendJob extends PushSendJob implements InjectableType { - private static final long serialVersionUID = 1L; + public static final String KEY = "PushGroupSendJob"; private static final String TAG = PushGroupSendJob.class.getSimpleName(); - @Inject transient SignalServiceMessageSender messageSender; + @Inject SignalServiceMessageSender messageSender; private static final String KEY_MESSAGE_ID = "message_id"; private static final String KEY_FILTER_ADDRESS = "filter_address"; private long messageId; - private long filterRecipientId; // Deprecated private String filterAddress; - public PushGroupSendJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public PushGroupSendJob(long messageId, @NonNull Address destination, @Nullable Address filterAddress) { + this(new Job.Parameters.Builder() + .setQueue(destination.toGroupString()) + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + messageId, filterAddress); + } - public PushGroupSendJob(Context context, long messageId, @NonNull Address destination, @Nullable Address filterAddress) { - super(context, JobParameters.newBuilder() - .withGroupId(destination.toGroupString()) - .withNetworkRequirement() - .withRetryDuration(TimeUnit.DAYS.toMillis(1)) - .create()); + private PushGroupSendJob(@NonNull Job.Parameters parameters, long messageId, @Nullable Address filterAddress) { + super(parameters); - this.messageId = messageId; - this.filterAddress = filterAddress == null ? null :filterAddress.toPhoneString(); - this.filterRecipientId = -1; + this.messageId = messageId; + this.filterAddress = filterAddress == null ? null :filterAddress.toPhoneString(); } @WorkerThread @@ -103,15 +98,14 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { attachments.addAll(Stream.of(message.getLinkPreviews()).filter(p -> p.getThumbnail().isPresent()).map(p -> p.getThumbnail().get()).toList()); attachments.addAll(Stream.of(message.getSharedContacts()).filter(c -> c.getAvatar() != null).map(c -> c.getAvatar().getAttachment()).withoutNulls().toList()); - List attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(context, ((DatabaseAttachment) a).getAttachmentId())).toList(); - ChainParameters chainParams = new ChainParameters.Builder().setGroupId(destination.serialize()).build(); + List attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId())).toList(); if (attachmentJobs.isEmpty()) { - jobManager.add(new PushGroupSendJob(context, messageId, destination, filterAddress)); + jobManager.add(new PushGroupSendJob(messageId, destination, filterAddress)); } else { jobManager.startChain(attachmentJobs) - .then(new PushGroupSendJob(context, messageId, destination, filterAddress)) - .enqueue(chainParams); + .then(new PushGroupSendJob(messageId, destination, filterAddress)) + .enqueue(); } } catch (NoSuchMessageException | MmsException e) { @@ -122,20 +116,19 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { } @Override - protected void initialize(@NonNull SafeData data) { - messageId = data.getLong(KEY_MESSAGE_ID); - filterAddress = data.getString(KEY_FILTER_ADDRESS); + public @NonNull Data serialize() { + return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId) + .putString(KEY_FILTER_ADDRESS, filterAddress) + .build(); } @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putLong(KEY_MESSAGE_ID, messageId) - .putString(KEY_FILTER_ADDRESS, filterAddress) - .build(); + public @NonNull String getFactoryKey() { + return KEY; } @Override - protected void onAdded() { + public void onAdded() { DatabaseFactory.getMmsDatabase(context).markAsSending(messageId); } @@ -287,4 +280,14 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false); return Stream.of(members).map(Recipient::getAddress).toList(); } + + public static class Factory implements Job.Factory { + @Override + public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull org.thoughtcrime.securesms.jobmanager.Data data) { + String address = data.getString(KEY_FILTER_ADDRESS); + Address filter = address != null ? Address.fromSerialized(data.getString(KEY_FILTER_ADDRESS)) : null; + + return new PushGroupSendJob(parameters, data.getLong(KEY_MESSAGE_ID), filter); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java index 420ea3973..4935e1801 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; @@ -10,8 +9,9 @@ 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.recipients.Recipient; import org.thoughtcrime.securesms.util.GroupUtil; @@ -34,52 +34,47 @@ import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class PushGroupUpdateJob extends BaseJob implements InjectableType { -public class PushGroupUpdateJob extends ContextJob implements InjectableType { + public static final String KEY = "PushGroupUpdateJob"; private static final String TAG = PushGroupUpdateJob.class.getSimpleName(); - private static final long serialVersionUID = 0L; - private static final String KEY_SOURCE = "source"; private static final String KEY_GROUP_ID = "group_id"; - @Inject transient SignalServiceMessageSender messageSender; + @Inject SignalServiceMessageSender messageSender; private String source; private byte[] groupId; - public PushGroupUpdateJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public PushGroupUpdateJob(String source, byte[] groupId) { + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + source, + groupId); } - public PushGroupUpdateJob(Context context, String source, byte[] groupId) { - super(context, JobParameters.newBuilder() - .withNetworkRequirement() - .withRetryDuration(TimeUnit.DAYS.toMillis(1)) - .create()); + private PushGroupUpdateJob(@NonNull Job.Parameters parameters, String source, byte[] groupId) { + super(parameters); this.source = source; this.groupId = groupId; } @Override - protected void initialize(@NonNull SafeData data) { - source = data.getString(KEY_SOURCE); - 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_SOURCE, source) + .putString(KEY_GROUP_ID, GroupUtil.getEncodedId(groupId, false)) + .build(); } @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putString(KEY_SOURCE, source) - .putString(KEY_GROUP_ID, GroupUtil.getEncodedId(groupId, false)) - .build(); + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -138,4 +133,17 @@ public class PushGroupUpdateJob extends ContextJob implements InjectableType { public void onCanceled() { } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull PushGroupUpdateJob create(@NonNull Parameters parameters, @NonNull org.thoughtcrime.securesms.jobmanager.Data data) { + try { + return new PushGroupUpdateJob(parameters, + data.getString(KEY_SOURCE), + GroupUtil.getDecodedId(data.getString(KEY_GROUP_ID))); + } catch (IOException e) { + throw new AssertionError(e); + } + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java index fcbead537..e09de2e3a 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -17,9 +17,9 @@ import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode; import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.ChainParameters; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.thoughtcrime.securesms.jobmanager.SafeData; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; @@ -48,27 +48,24 @@ import java.util.List; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; - public class PushMediaSendJob extends PushSendJob implements InjectableType { - private static final long serialVersionUID = 1L; + public static final String KEY = "PushMediaSendJob"; private static final String TAG = PushMediaSendJob.class.getSimpleName(); private static final String KEY_MESSAGE_ID = "message_id"; - @Inject transient SignalServiceMessageSender messageSender; + @Inject SignalServiceMessageSender messageSender; private long messageId; - public PushMediaSendJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public PushMediaSendJob(long messageId, Address destination) { + this(constructParameters(destination), messageId); } - public PushMediaSendJob(Context context, long messageId, Address destination) { - super(context, constructParameters(destination)); + private PushMediaSendJob(Job.Parameters parameters, long messageId) { + super(parameters); this.messageId = messageId; } @@ -83,15 +80,14 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { attachments.addAll(Stream.of(message.getLinkPreviews()).filter(p -> p.getThumbnail().isPresent()).map(p -> p.getThumbnail().get()).toList()); attachments.addAll(Stream.of(message.getSharedContacts()).filter(c -> c.getAvatar() != null).map(c -> c.getAvatar().getAttachment()).withoutNulls().toList()); - List attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(context, ((DatabaseAttachment) a).getAttachmentId())).toList(); - ChainParameters chainParams = new ChainParameters.Builder().setGroupId(destination.serialize()).build(); + List attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId())).toList(); if (attachmentJobs.isEmpty()) { - jobManager.add(new PushMediaSendJob(context, messageId, destination)); + jobManager.add(new PushMediaSendJob(messageId, destination)); } else { jobManager.startChain(attachmentJobs) - .then(new PushMediaSendJob(context, messageId, destination)) - .enqueue(chainParams); + .then(new PushMediaSendJob(messageId, destination)) + .enqueue(); } } catch (NoSuchMessageException | MmsException e) { @@ -102,17 +98,17 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { } @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 - protected void onAdded() { + public void onAdded() { DatabaseFactory.getMmsDatabase(context).markAsSending(messageId); } @@ -173,7 +169,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { warn(TAG, "Failure", ifae); database.markAsPendingInsecureSmsFallback(messageId); notifyMediaMessageDeliveryFailed(context, messageId); - ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(context, false)); + ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(false)); } catch (UntrustedIdentityException uie) { warn(TAG, "Failure", uie); database.addMismatchedIdentity(messageId, Address.fromSerialized(uie.getE164Number()), uie.getIdentityKey()); @@ -242,4 +238,11 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { throw new RetryLaterException(e); } } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull PushMediaSendJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new PushMediaSendJob(parameters, data.getLong(KEY_MESSAGE_ID)); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushNotificationReceiveJob.java b/src/org/thoughtcrime/securesms/jobs/PushNotificationReceiveJob.java index 524e42b6c..fd1af3f61 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushNotificationReceiveJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushNotificationReceiveJob.java @@ -3,10 +3,10 @@ package org.thoughtcrime.securesms.jobs; import android.content.Context; import android.support.annotation.NonNull; -import org.thoughtcrime.securesms.R; 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.signalservice.api.SignalServiceMessageReceiver; @@ -16,33 +16,36 @@ import java.io.IOException; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; - public class PushNotificationReceiveJob extends PushReceivedJob implements InjectableType { + public static final String KEY = "PushNotificationReceiveJob"; + private static final String TAG = PushNotificationReceiveJob.class.getSimpleName(); - @Inject transient SignalServiceMessageReceiver receiver; - - public PushNotificationReceiveJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); - } + @Inject SignalServiceMessageReceiver receiver; public PushNotificationReceiveJob(Context context) { - super(context, JobParameters.newBuilder() - .withNetworkRequirement() - .withGroupId("__notification_received") - .create()); + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setQueue("__notification_received") + .setMaxAttempts(3) + .setMaxInstances(1) + .build()); + setContext(context); + } + + private PushNotificationReceiveJob(@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 @@ -75,4 +78,11 @@ public class PushNotificationReceiveJob extends PushReceivedJob implements Injec private static String timeSuffix(long startTime) { return " (" + (System.currentTimeMillis() - startTime) + " ms elapsed)"; } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull PushNotificationReceiveJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new PushNotificationReceiveJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java b/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java index 74a605f59..53641a080 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.jobs; import android.annotation.SuppressLint; -import android.content.Context; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.ApplicationContext; @@ -9,25 +8,19 @@ import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.jobmanager.JobParameters; +import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; -import androidx.work.WorkerParameters; - -public abstract class PushReceivedJob extends ContextJob { +public abstract class PushReceivedJob extends BaseJob { private static final String TAG = PushReceivedJob.class.getSimpleName(); public static final Object RECEIVE_LOCK = new Object(); - protected PushReceivedJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); - } - - protected PushReceivedJob(Context context, JobParameters parameters) { - super(context, parameters); + protected PushReceivedJob(Job.Parameters parameters) { + super(parameters); } public void processEnvelope(@NonNull SignalServiceEnvelope envelope) { @@ -38,7 +31,7 @@ public abstract class PushReceivedJob extends ContextJob { if (!isActiveNumber(recipient)) { DatabaseFactory.getRecipientDatabase(context).setRegistered(recipient, RecipientDatabase.RegisteredState.REGISTERED); - ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(context, recipient, false)); + ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(recipient, false)); } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java index e53865bd2..ac0ea2afc 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -19,7 +19,8 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.events.PartProgressEvent; -import org.thoughtcrime.securesms.jobmanager.JobParameters; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; @@ -51,29 +52,22 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; -import androidx.work.WorkerParameters; - public abstract class PushSendJob extends SendJob { - private static final long serialVersionUID = 5906098204770900739L; private static final String TAG = PushSendJob.class.getSimpleName(); private static final long CERTIFICATE_EXPIRATION_BUFFER = TimeUnit.DAYS.toMillis(1); - public PushSendJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + protected PushSendJob(Job.Parameters parameters) { + super(parameters); } - protected PushSendJob(Context context, JobParameters parameters) { - super(context, parameters); - } - - protected static JobParameters constructParameters(Address destination) { - JobParameters.Builder builder = JobParameters.newBuilder(); - builder.withGroupId(destination.serialize()); - builder.withNetworkRequirement(); - builder.withRetryDuration(TimeUnit.DAYS.toMillis(1)); - - return builder.create(); + protected static Job.Parameters constructParameters(Address destination) { + return new Parameters.Builder() + .setQueue(destination.serialize()) + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build(); } @Override @@ -81,7 +75,7 @@ public abstract class PushSendJob extends SendJob { if (TextSecurePreferences.getSignedPreKeyFailureCount(context) > 5) { ApplicationContext.getInstance(context) .getJobManager() - .add(new RotateSignedPreKeyJob(context)); + .add(new RotateSignedPreKeyJob()); throw new TextSecureExpiredException("Too many signed prekey rotation failures"); } @@ -94,9 +88,9 @@ public abstract class PushSendJob extends SendJob { super.onRetry(); Log.i(TAG, "onRetry()"); - if (getRunAttemptCount() > 1) { + if (getRunAttempt() > 1) { Log.i(TAG, "Scheduling service outage detection job."); - ApplicationContext.getInstance(context).getJobManager().add(new ServiceOutageDetectionJob(context)); + ApplicationContext.getInstance(context).getJobManager().add(new ServiceOutageDetectionJob()); } } @@ -278,7 +272,6 @@ public abstract class PushSendJob extends SendJob { Log.w(TAG, "Certificate was invalid at send time. Fetching a new one.", e); RotateCertificateJob certificateJob = new RotateCertificateJob(context); ApplicationContext.getInstance(context).injectDependencies(certificateJob); - certificateJob.setContext(context); certificateJob.onRun(); } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index 38dfda766..55532af70 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -1,11 +1,9 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode; -import org.thoughtcrime.securesms.jobmanager.SafeData; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; @@ -15,6 +13,8 @@ import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.dependencies.InjectableType; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.ExpiringMessageManager; @@ -34,38 +34,36 @@ import java.io.IOException; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; - public class PushTextSendJob extends PushSendJob implements InjectableType { - private static final long serialVersionUID = 1L; + public static final String KEY = "PushTextSendJob"; private static final String TAG = PushTextSendJob.class.getSimpleName(); private static final String KEY_MESSAGE_ID = "message_id"; - @Inject transient SignalServiceMessageSender messageSender; + @Inject SignalServiceMessageSender messageSender; private long messageId; - public PushTextSendJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public PushTextSendJob(long messageId, Address destination) { + this(constructParameters(destination), messageId); } - public PushTextSendJob(Context context, long messageId, Address destination) { - super(context, constructParameters(destination)); + private PushTextSendJob(@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(); } + @NonNull @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putLong(KEY_MESSAGE_ID, messageId).build(); + public String getFactoryKey() { + return KEY; } @Override @@ -126,7 +124,7 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { warn(TAG, "Failure", e); database.markAsPendingInsecureSmsFallback(record.getId()); MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipient(), record.getThreadId()); - ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(context, false)); + ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(false)); } catch (UntrustedIdentityException e) { warn(TAG, "Failure", e); database.addMismatchedIdentity(record.getId(), Address.fromSerialized(e.getE164Number()), e.getIdentityKey()); @@ -191,4 +189,11 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { throw new RetryLaterException(e); } } + + public static class Factory implements Job.Factory { + @Override + public @NonNull PushTextSendJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new PushTextSendJob(parameters, data.getLong(KEY_MESSAGE_ID)); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/src/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index 5b5b71805..058960a8b 100644 --- a/src/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -1,14 +1,14 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.ApplicationContext; -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.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.JobParameters; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -19,35 +19,33 @@ import java.io.IOException; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class RefreshAttributesJob extends BaseJob implements InjectableType { -public class RefreshAttributesJob extends ContextJob implements InjectableType { - - public static final long serialVersionUID = 1L; + public static final String KEY = "RefreshAttributesJob"; private static final String TAG = RefreshAttributesJob.class.getSimpleName(); - @Inject transient SignalServiceAccountManager signalAccountManager; + @Inject SignalServiceAccountManager signalAccountManager; - public RefreshAttributesJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public RefreshAttributesJob() { + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setQueue("RefreshAttributesJob") + .build()); } - public RefreshAttributesJob(Context context) { - super(context, JobParameters.newBuilder() - .withNetworkRequirement() - .withGroupId(RefreshAttributesJob.class.getName()) - .create()); + private RefreshAttributesJob(@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 @@ -63,7 +61,7 @@ public class RefreshAttributesJob extends ContextJob implements InjectableType { ApplicationContext.getInstance(context) .getJobManager() - .add(new RefreshUnidentifiedDeliveryAbilityJob(context)); + .add(new RefreshUnidentifiedDeliveryAbilityJob()); } @Override @@ -75,4 +73,11 @@ public class RefreshAttributesJob extends ContextJob implements InjectableType { public void onCanceled() { Log.w(TAG, "Failed to update account attributes!"); } + + public static class Factory implements Job.Factory { + @Override + public @NonNull RefreshAttributesJob create(@NonNull Parameters parameters, @NonNull org.thoughtcrime.securesms.jobmanager.Data data) { + return new RefreshAttributesJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java b/src/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java index e37764431..362942107 100644 --- a/src/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java @@ -1,15 +1,14 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.ApplicationContext; 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; @@ -24,36 +23,36 @@ import java.util.List; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class RefreshPreKeysJob extends BaseJob implements InjectableType { -public class RefreshPreKeysJob extends ContextJob implements InjectableType { + public static final String KEY = "RefreshPreKeysJob"; private static final String TAG = RefreshPreKeysJob.class.getSimpleName(); private static final int PREKEY_MINIMUM = 10; - @Inject transient SignalServiceAccountManager accountManager; + @Inject SignalServiceAccountManager accountManager; - public RefreshPreKeysJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public RefreshPreKeysJob() { + this(new Job.Parameters.Builder() + .setQueue("RefreshPreKeysJob") + .addConstraint(NetworkConstraint.KEY) + .setMaxAttempts(5) + .build()); } - public RefreshPreKeysJob(Context context) { - super(context, JobParameters.newBuilder() - .withGroupId(RefreshPreKeysJob.class.getSimpleName()) - .withNetworkRequirement() - .withRetryCount(5) - .create()); + private RefreshPreKeysJob(@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 @@ -80,7 +79,7 @@ public class RefreshPreKeysJob extends ContextJob implements InjectableType { ApplicationContext.getInstance(context) .getJobManager() - .add(new CleanPreKeysJob(context)); + .add(new CleanPreKeysJob()); } @Override @@ -93,7 +92,12 @@ public class RefreshPreKeysJob extends ContextJob implements InjectableType { @Override public void onCanceled() { - } + public static final class Factory implements Job.Factory { + @Override + public @NonNull RefreshPreKeysJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new RefreshPreKeysJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/RefreshUnidentifiedDeliveryAbilityJob.java b/src/org/thoughtcrime/securesms/jobs/RefreshUnidentifiedDeliveryAbilityJob.java index 8f210a3ea..b3977a07c 100644 --- a/src/org/thoughtcrime/securesms/jobs/RefreshUnidentifiedDeliveryAbilityJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RefreshUnidentifiedDeliveryAbilityJob.java @@ -1,12 +1,12 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; 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.service.IncomingMessageObserver; import org.thoughtcrime.securesms.util.Base64; @@ -23,31 +23,33 @@ import java.io.IOException; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class RefreshUnidentifiedDeliveryAbilityJob extends BaseJob implements InjectableType { -public class RefreshUnidentifiedDeliveryAbilityJob extends ContextJob implements InjectableType { + public static final String KEY = "RefreshUnidentifiedDeliveryAbilityJob"; private static final String TAG = RefreshUnidentifiedDeliveryAbilityJob.class.getSimpleName(); - @Inject transient SignalServiceMessageReceiver receiver; + @Inject SignalServiceMessageReceiver receiver; - public RefreshUnidentifiedDeliveryAbilityJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public RefreshUnidentifiedDeliveryAbilityJob() { + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setMaxAttempts(10) + .build()); } - public RefreshUnidentifiedDeliveryAbilityJob(Context context) { - super(context, new JobParameters.Builder() - .withNetworkRequirement() - .create()); + private RefreshUnidentifiedDeliveryAbilityJob(@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 @@ -62,8 +64,7 @@ public class RefreshUnidentifiedDeliveryAbilityJob extends ContextJob implements } @Override - protected void onCanceled() { - + public void onCanceled() { } @Override @@ -94,4 +95,11 @@ public class RefreshUnidentifiedDeliveryAbilityJob extends ContextJob implements return false; } } + + public static class Factory implements Job.Factory { + @Override + public @NonNull RefreshUnidentifiedDeliveryAbilityJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new RefreshUnidentifiedDeliveryAbilityJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java b/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java index 6ff58a84c..31dd8b81f 100644 --- a/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java @@ -1,15 +1,14 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.Address; 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.util.GroupUtil; -import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement; import org.thoughtcrime.securesms.recipients.Recipient; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; @@ -20,56 +19,53 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import java.io.IOException; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class RequestGroupInfoJob extends BaseJob implements InjectableType { -public class RequestGroupInfoJob extends ContextJob implements InjectableType { + public static final String KEY = "RequestGroupInfoJob"; @SuppressWarnings("unused") private static final String TAG = RequestGroupInfoJob.class.getSimpleName(); - private static final long serialVersionUID = 0L; - private static final String KEY_SOURCE = "source"; private static final String KEY_GROUP_ID = "group_id"; - @Inject transient SignalServiceMessageSender messageSender; + @Inject SignalServiceMessageSender messageSender; private String source; private byte[] groupId; - public RequestGroupInfoJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public RequestGroupInfoJob(@NonNull String source, @NonNull byte[] groupId) { + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + source, + groupId); + } - public RequestGroupInfoJob(@NonNull Context context, @NonNull String source, @NonNull byte[] groupId) { - super(context, JobParameters.newBuilder() - .withNetworkRequirement() - .withRetryCount(50) - .create()); + private RequestGroupInfoJob(@NonNull Job.Parameters parameters, @NonNull String source, @NonNull byte[] groupId) { + super(parameters); this.source = source; this.groupId = groupId; } @Override - protected void initialize(@NonNull SafeData data) { - source = data.getString(KEY_SOURCE); - 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_SOURCE, source) + .putString(KEY_GROUP_ID, GroupUtil.getEncodedId(groupId, false)) + .build(); } @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putString(KEY_SOURCE, source) - .putString(KEY_GROUP_ID, GroupUtil.getEncodedId(groupId, false)) - .build(); + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -97,4 +93,18 @@ public class RequestGroupInfoJob extends ContextJob implements InjectableType { public void onCanceled() { } + + public static final class Factory implements Job.Factory { + + @Override + public @NonNull RequestGroupInfoJob create(@NonNull Parameters parameters, @NonNull Data data) { + try { + return new RequestGroupInfoJob(parameters, + data.getString(KEY_SOURCE), + GroupUtil.getDecodedId(data.getString(KEY_GROUP_ID))); + } catch (IOException e) { + throw new AssertionError(e); + } + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java index 45f38f1b3..443c11ea3 100644 --- a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java @@ -1,18 +1,19 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; +import android.app.Application; import android.support.annotation.NonNull; import android.text.TextUtils; 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.database.DatabaseFactory; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.JobParameters; import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.Util; @@ -23,13 +24,13 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType { -public class RetrieveProfileAvatarJob extends ContextJob implements InjectableType { + public static final String KEY = "RetrieveProfileAvatarJob"; private static final String TAG = RetrieveProfileAvatarJob.class.getSimpleName(); @@ -43,32 +44,34 @@ public class RetrieveProfileAvatarJob extends ContextJob implements InjectableTy private String profileAvatar; private Recipient recipient; - public RetrieveProfileAvatarJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) { + this(new Job.Parameters.Builder() + .setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize()) + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.HOURS.toMillis(1)) + .setMaxInstances(1) + .build(), + recipient, + profileAvatar); } - public RetrieveProfileAvatarJob(Context context, Recipient recipient, String profileAvatar) { - super(context, JobParameters.newBuilder() - .withGroupId(RetrieveProfileAvatarJob.class.getSimpleName() + recipient.getAddress().serialize()) - .withDuplicatesIgnored(true) - .withNetworkRequirement() - .create()); + private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient, String profileAvatar) { + super(parameters); this.recipient = recipient; this.profileAvatar = profileAvatar; } @Override - protected void initialize(@NonNull SafeData data) { - profileAvatar = data.getString(KEY_PROFILE_AVATAR); - recipient = Recipient.from(context, Address.fromSerialized(data.getString(KEY_ADDRESS)), true); + public @NonNull Data serialize() { + return new Data.Builder().putString(KEY_PROFILE_AVATAR, profileAvatar) + .putString(KEY_ADDRESS, recipient.getAddress().serialize()) + .build(); } @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putString(KEY_PROFILE_AVATAR, profileAvatar) - .putString(KEY_ADDRESS, recipient.getAddress().serialize()) - .build(); + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -110,13 +113,27 @@ public class RetrieveProfileAvatarJob extends ContextJob implements InjectableTy @Override public boolean onShouldRetry(Exception e) { - Log.w(TAG, e); if (e instanceof PushNetworkException) return true; return false; } @Override public void onCanceled() { + } + public static final class Factory implements Job.Factory { + + private final Application application; + + public Factory(Application application) { + this.application = application; + } + + @Override + public @NonNull RetrieveProfileAvatarJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new RetrieveProfileAvatarJob(parameters, + Recipient.from(application, Address.fromSerialized(data.getString(KEY_ADDRESS)), true), + data.getString(KEY_PROFILE_AVATAR)); + } } } diff --git a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index 73dbd716c..a09061671 100644 --- a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -1,7 +1,7 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; +import android.app.Application; import android.support.annotation.NonNull; import android.text.TextUtils; @@ -12,8 +12,9 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode; 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.recipients.Recipient; import org.thoughtcrime.securesms.service.IncomingMessageObserver; @@ -38,40 +39,39 @@ import java.util.List; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class RetrieveProfileJob extends BaseJob implements InjectableType { -public class RetrieveProfileJob extends ContextJob implements InjectableType { + public static final String KEY = "RetrieveProfileJob"; private static final String TAG = RetrieveProfileJob.class.getSimpleName(); private static final String KEY_ADDRESS = "address"; - @Inject transient SignalServiceMessageReceiver receiver; + @Inject SignalServiceMessageReceiver receiver; private Recipient recipient; - public RetrieveProfileJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public RetrieveProfileJob(@NonNull Recipient recipient) { + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setMaxAttempts(3) + .build(), + recipient); } - public RetrieveProfileJob(Context context, Recipient recipient) { - super(context, JobParameters.newBuilder() - .withNetworkRequirement() - .withRetryCount(3) - .create()); - + private RetrieveProfileJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient) { + super(parameters); this.recipient = recipient; } @Override - protected void initialize(@NonNull SafeData data) { - recipient = Recipient.from(context, Address.fromSerialized(data.getString(KEY_ADDRESS)), true); + public @NonNull Data serialize() { + return new Data.Builder().putString(KEY_ADDRESS, recipient.getAddress().serialize()).build(); } @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putString(KEY_ADDRESS, recipient.getAddress().serialize()).build(); + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -221,7 +221,7 @@ public class RetrieveProfileJob extends ContextJob implements InjectableType { if (!Util.equals(profileAvatar, recipient.getProfileAvatar())) { ApplicationContext.getInstance(context) .getJobManager() - .add(new RetrieveProfileAvatarJob(context, recipient, profileAvatar)); + .add(new RetrieveProfileAvatarJob(recipient, profileAvatar)); } } @@ -234,4 +234,18 @@ public class RetrieveProfileJob extends ContextJob implements InjectableType { return Optional.absent(); } + + public static final class Factory implements Job.Factory { + + private final Application application; + + public Factory(Application application) { + this.application = application; + } + + @Override + public @NonNull RetrieveProfileJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new RetrieveProfileJob(parameters, Recipient.from(application, Address.fromSerialized(data.getString(KEY_ADDRESS)), true)); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java b/src/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java index 9592b08a6..3a0270f80 100644 --- a/src/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java @@ -5,55 +5,55 @@ import android.content.Context; import android.support.annotation.NonNull; 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.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import java.io.IOException; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; - @SuppressWarnings("WeakerAccess") -public class RotateCertificateJob extends ContextJob implements InjectableType { +public class RotateCertificateJob extends BaseJob implements InjectableType { - private static final long serialVersionUID = 1L; + public static final String KEY = "RotateCertificateJob"; private static final String TAG = RotateCertificateJob.class.getSimpleName(); - @Inject transient SignalServiceAccountManager accountManager; - - public RotateCertificateJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); - } + @Inject SignalServiceAccountManager accountManager; public RotateCertificateJob(Context context) { - super(context, JobParameters.newBuilder() - .withGroupId("__ROTATE_SENDER_CERTIFICATE__") - .withNetworkRequirement() - .create()); + this(new Job.Parameters.Builder() + .setQueue("__ROTATE_SENDER_CERTIFICATE__") + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build()); + setContext(context); } - @NonNull - @Override - protected Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.build(); + private RotateCertificateJob(@NonNull Job.Parameters parameters) { + super(parameters); } @Override - protected void initialize(@NonNull SafeData data) { + public @NonNull Data serialize() { + return Data.EMPTY; + } + @Override + public String getFactoryKey() { + return KEY; } @Override public void onAdded() {} - @Override public void onRun() throws IOException { synchronized (RotateCertificateJob.class) { @@ -71,4 +71,11 @@ public class RotateCertificateJob extends ContextJob implements InjectableType { public void onCanceled() { Log.w(TAG, "Failed to rotate sender certificate!"); } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull RotateCertificateJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new RotateCertificateJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java b/src/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java index 895f9475a..3124ad4d2 100644 --- a/src/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -8,8 +7,9 @@ import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.database.Address; 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.profiles.AvatarHelper; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.SignalServiceAccountManager; @@ -22,33 +22,33 @@ import java.io.IOException; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class RotateProfileKeyJob extends BaseJob implements InjectableType { -public class RotateProfileKeyJob extends ContextJob implements InjectableType { + public static String KEY = "RotateProfileKeyJob"; @Inject SignalServiceAccountManager accountManager; - public RotateProfileKeyJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public RotateProfileKeyJob() { + this(new Job.Parameters.Builder() + .setQueue("__ROTATE_PROFILE_KEY__") + .addConstraint(NetworkConstraint.KEY) + .setMaxAttempts(25) + .setMaxInstances(1) + .build()); } - public RotateProfileKeyJob(Context context) { - super(context, new JobParameters.Builder() - .withGroupId("__ROTATE_PROFILE_KEY__") - .withDuplicatesIgnored(true) - .withNetworkRequirement() - .create()); - } - - @NonNull - @Override - protected Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.build(); + private RotateProfileKeyJob(@NonNull Job.Parameters parameters) { + super(parameters); } @Override - protected void initialize(@NonNull SafeData data) { + public @NonNull Data serialize() { + return Data.EMPTY; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -60,11 +60,11 @@ public class RotateProfileKeyJob extends ContextJob implements InjectableType { ApplicationContext.getInstance(context) .getJobManager() - .add(new RefreshAttributesJob(context)); + .add(new RefreshAttributesJob()); } @Override - protected void onCanceled() { + public void onCanceled() { } @@ -86,4 +86,11 @@ public class RotateProfileKeyJob extends ContextJob implements InjectableType { } return null; } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull RotateProfileKeyJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new RotateProfileKeyJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java b/src/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java index 9db70110a..9f1ccdb0f 100644 --- a/src/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java @@ -1,16 +1,15 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.ApplicationContext; 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,33 +19,33 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class RotateSignedPreKeyJob extends BaseJob implements InjectableType { -public class RotateSignedPreKeyJob extends ContextJob implements InjectableType { + public static final String KEY = "RotateSignedPreKeyJob"; private static final String TAG = RotateSignedPreKeyJob.class.getSimpleName(); - @Inject transient SignalServiceAccountManager accountManager; + @Inject SignalServiceAccountManager accountManager; - public RotateSignedPreKeyJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public RotateSignedPreKeyJob() { + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setMaxAttempts(5) + .build()); } - public RotateSignedPreKeyJob(Context context) { - super(context, JobParameters.newBuilder() - .withNetworkRequirement() - .withRetryCount(5) - .create()); + private RotateSignedPreKeyJob(@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 @@ -64,7 +63,7 @@ public class RotateSignedPreKeyJob extends ContextJob implements InjectableType ApplicationContext.getInstance(context) .getJobManager() - .add(new CleanPreKeysJob(context)); + .add(new CleanPreKeysJob()); } @Override @@ -76,4 +75,11 @@ public class RotateSignedPreKeyJob extends ContextJob implements InjectableType public void onCanceled() { TextSecurePreferences.setSignedPreKeyFailureCount(context, TextSecurePreferences.getSignedPreKeyFailureCount(context) + 1); } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull RotateSignedPreKeyJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new RotateSignedPreKeyJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java b/src/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java index c4a418342..0a0711f5b 100644 --- a/src/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java @@ -1,14 +1,14 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.Address; 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.recipients.Recipient; import org.whispersystems.signalservice.api.SignalServiceMessageSender; @@ -19,15 +19,13 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import java.io.IOException; import java.util.Collections; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class SendDeliveryReceiptJob extends BaseJob implements InjectableType { -public class SendDeliveryReceiptJob extends ContextJob implements InjectableType { - - private static final long serialVersionUID = 1L; + public static final String KEY = "SendDeliveryReceiptJob"; private static final String KEY_ADDRESS = "address"; private static final String KEY_MESSAGE_ID = "message_id"; @@ -42,37 +40,40 @@ public class SendDeliveryReceiptJob extends ContextJob implements InjectableType private long messageId; private long timestamp; - public SendDeliveryReceiptJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public SendDeliveryReceiptJob(@NonNull Address address, long messageId) { + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + address, + messageId, + System.currentTimeMillis()); } - public SendDeliveryReceiptJob(Context context, Address address, long messageId) { - super(context, JobParameters.newBuilder() - .withNetworkRequirement() - .create()); + private SendDeliveryReceiptJob(@NonNull Job.Parameters parameters, + @NonNull Address address, + long messageId, + long timestamp) + { + super(parameters); this.address = address.serialize(); this.messageId = messageId; - this.timestamp = System.currentTimeMillis(); + this.timestamp = timestamp; } @Override - public void onAdded() {} - - @NonNull - @Override - protected Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putString(KEY_ADDRESS, address) - .putLong(KEY_MESSAGE_ID, messageId) - .putLong(KEY_TIMESTAMP, timestamp) - .build(); + public @NonNull Data serialize() { + return new Data.Builder().putString(KEY_ADDRESS, address) + .putLong(KEY_MESSAGE_ID, messageId) + .putLong(KEY_TIMESTAMP, timestamp) + .build(); } @Override - protected void initialize(@NonNull SafeData data) { - this.address = data.getString(KEY_ADDRESS); - this.messageId = data.getLong(KEY_MESSAGE_ID); - this.timestamp = data.getLong(KEY_TIMESTAMP); + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -98,4 +99,13 @@ public class SendDeliveryReceiptJob extends ContextJob implements InjectableType Log.w(TAG, "Failed to send delivery receipt to: " + address); } + public static final class Factory implements Job.Factory { + @Override + public @NonNull SendDeliveryReceiptJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new SendDeliveryReceiptJob(parameters, + Address.fromSerialized(data.getString(KEY_ADDRESS)), + data.getLong(KEY_MESSAGE_ID), + data.getLong(KEY_TIMESTAMP)); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/SendJob.java b/src/org/thoughtcrime/securesms/jobs/SendJob.java index b29f41b0d..b6d989c89 100644 --- a/src/org/thoughtcrime/securesms/jobs/SendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/SendJob.java @@ -1,17 +1,15 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import org.thoughtcrime.securesms.BuildConfig; -import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.TextSecureExpiredException; import org.thoughtcrime.securesms.attachments.Attachment; -import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.jobmanager.JobParameters; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JobLogger; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MediaStream; @@ -24,19 +22,13 @@ import java.io.IOException; import java.util.LinkedList; import java.util.List; -import androidx.work.WorkerParameters; - -public abstract class SendJob extends ContextJob { +public abstract class SendJob extends BaseJob { @SuppressWarnings("unused") private final static String TAG = SendJob.class.getSimpleName(); - public SendJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); - } - - public SendJob(Context context, JobParameters parameters) { - super(context, parameters); + public SendJob(Job.Parameters parameters) { + super(parameters); } @Override @@ -93,7 +85,7 @@ public abstract class SendJob extends ContextJob { } protected void log(@NonNull String tag, @NonNull String message) { - Log.i(tag, "[" + getId().toString() + "] " + message + logSuffix()); + Log.i(tag, JobLogger.format(this, message)); } protected void warn(@NonNull String tag, @NonNull String message) { @@ -105,6 +97,6 @@ public abstract class SendJob extends ContextJob { } protected void warn(@NonNull String tag, @NonNull String message, @Nullable Throwable t) { - Log.w(tag, "[" + getId().toString() + "] " + message + logSuffix(), t); + Log.w(tag, JobLogger.format(this, message), t); } } diff --git a/src/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java b/src/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java index dbea51986..d7ea76b4d 100644 --- a/src/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java @@ -1,14 +1,14 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.Address; 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.recipients.Recipient; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -21,15 +21,13 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class SendReadReceiptJob extends BaseJob implements InjectableType { -public class SendReadReceiptJob extends ContextJob implements InjectableType { - - private static final long serialVersionUID = 1L; + public static final String KEY = "SendReadReceiptJob"; private static final String TAG = SendReadReceiptJob.class.getSimpleName(); @@ -37,49 +35,51 @@ public class SendReadReceiptJob extends ContextJob implements InjectableType { private static final String KEY_MESSAGE_IDS = "message_ids"; private static final String KEY_TIMESTAMP = "timestamp"; - @Inject transient SignalServiceMessageSender messageSender; + @Inject SignalServiceMessageSender messageSender; private String address; private List messageIds; private long timestamp; - public SendReadReceiptJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public SendReadReceiptJob(Address address, List messageIds) { + this(new Job.Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + address, + messageIds, + System.currentTimeMillis()); } - public SendReadReceiptJob(Context context, Address address, List messageIds) { - super(context, JobParameters.newBuilder() - .withNetworkRequirement() - .create()); + private SendReadReceiptJob(@NonNull Job.Parameters parameters, + @NonNull Address address, + @NonNull List messageIds, + long timestamp) + { + super(parameters); this.address = address.serialize(); this.messageIds = messageIds; - this.timestamp = System.currentTimeMillis(); + this.timestamp = timestamp; } @Override - protected void initialize(@NonNull SafeData data) { - address = data.getString(KEY_ADDRESS); - timestamp = data.getLong(KEY_TIMESTAMP); - - long[] ids = data.getLongArray(KEY_MESSAGE_IDS); - messageIds = new ArrayList<>(ids.length); - for (long id : ids) { - messageIds.add(id); - } - } - - @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { + public @NonNull Data serialize() { long[] ids = new long[messageIds.size()]; for (int i = 0; i < ids.length; i++) { ids[i] = messageIds.get(i); } - return dataBuilder.putString(KEY_ADDRESS, address) - .putLongArray(KEY_MESSAGE_IDS, ids) - .putLong(KEY_TIMESTAMP, timestamp) - .build(); + return new Data.Builder().putString(KEY_ADDRESS, address) + .putLongArray(KEY_MESSAGE_IDS, ids) + .putLong(KEY_TIMESTAMP, timestamp) + .build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -104,4 +104,20 @@ public class SendReadReceiptJob extends ContextJob implements InjectableType { public void onCanceled() { Log.w(TAG, "Failed to send read receipts to: " + address); } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull SendReadReceiptJob create(@NonNull Parameters parameters, @NonNull Data data) { + Address address = Address.fromSerialized(data.getString(KEY_ADDRESS)); + long timestamp = data.getLong(KEY_TIMESTAMP); + long[] ids = data.getLongArray(KEY_MESSAGE_IDS); + List messageIds = new ArrayList<>(ids.length); + + for (long id : ids) { + messageIds.add(id); + } + + return new SendReadReceiptJob(parameters, address, messageIds, timestamp); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/ServiceOutageDetectionJob.java b/src/org/thoughtcrime/securesms/jobs/ServiceOutageDetectionJob.java index 21b79bf37..47dbd6a77 100644 --- a/src/org/thoughtcrime/securesms/jobs/ServiceOutageDetectionJob.java +++ b/src/org/thoughtcrime/securesms/jobs/ServiceOutageDetectionJob.java @@ -1,13 +1,13 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import org.greenrobot.eventbus.EventBus; import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.events.ReminderUpdateEvent; -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.transport.RetryLaterException; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -15,10 +15,9 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import java.net.InetAddress; import java.net.UnknownHostException; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class ServiceOutageDetectionJob extends BaseJob { -public class ServiceOutageDetectionJob extends ContextJob { + public static final String KEY = "ServiceOutageDetectionJob"; private static final String TAG = ServiceOutageDetectionJob.class.getSimpleName(); @@ -26,26 +25,27 @@ public class ServiceOutageDetectionJob extends ContextJob { private static final String IP_FAILURE = "127.0.0.2"; private static final long CHECK_TIME = 1000 * 60; - public ServiceOutageDetectionJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public ServiceOutageDetectionJob() { + this(new Job.Parameters.Builder() + .setQueue("ServiceOutageDetectionJob") + .addConstraint(NetworkConstraint.KEY) + .setMaxAttempts(5) + .setMaxInstances(1) + .build()); } - public ServiceOutageDetectionJob(Context context) { - super(context, new JobParameters.Builder() - .withGroupId(ServiceOutageDetectionJob.class.getSimpleName()) - .withDuplicatesIgnored(true) - .withNetworkRequirement() - .withRetryCount(5) - .create()); + private ServiceOutageDetectionJob(@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 @@ -92,4 +92,11 @@ public class ServiceOutageDetectionJob extends ContextJob { TextSecurePreferences.setLastOutageCheckTime(context, System.currentTimeMillis()); EventBus.getDefault().post(new ReminderUpdateEvent()); } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull ServiceOutageDetectionJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new ServiceOutageDetectionJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java b/src/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java index 79513f872..fceeaae38 100644 --- a/src/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java +++ b/src/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java @@ -1,17 +1,17 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.telephony.SmsMessage; -import org.thoughtcrime.securesms.jobmanager.SafeData; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; import org.thoughtcrime.securesms.database.SmsDatabase; -import org.thoughtcrime.securesms.jobmanager.JobParameters; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.sms.IncomingTextMessage; @@ -23,12 +23,9 @@ import java.io.IOException; import java.util.LinkedList; import java.util.List; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class SmsReceiveJob extends BaseJob { -public class SmsReceiveJob extends ContextJob { - - private static final long serialVersionUID = 1L; + public static final String KEY = "SmsReceiveJob"; private static final String TAG = SmsReceiveJob.class.getSimpleName(); @@ -39,44 +36,37 @@ public class SmsReceiveJob extends ContextJob { private int subscriptionId; - public SmsReceiveJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public SmsReceiveJob(@Nullable Object[] pdus, int subscriptionId) { + this(new Job.Parameters.Builder() + .addConstraint(SqlCipherMigrationConstraint.KEY) + .setMaxAttempts(25) + .build(), + pdus, + subscriptionId); } - public SmsReceiveJob(@NonNull Context context, @Nullable Object[] pdus, int subscriptionId) { - super(context, JobParameters.newBuilder() - .withSqlCipherRequirement() - .create()); + private SmsReceiveJob(@NonNull Job.Parameters parameters, @Nullable Object[] pdus, int subscriptionId) { + super(parameters); this.pdus = pdus; this.subscriptionId = subscriptionId; } @Override - protected void initialize(@NonNull SafeData data) { - String[] encoded = data.getStringArray(KEY_PDUS); - pdus = new Object[encoded.length]; - try { - for (int i = 0; i < encoded.length; i++) { - pdus[i] = Base64.decode(encoded[i]); - } - } catch (IOException e) { - throw new AssertionError(e); - } - - subscriptionId = data.getInt(KEY_SUBSCRIPTION_ID); - } - - @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { + public @NonNull Data serialize() { String[] encoded = new String[pdus.length]; for (int i = 0; i < pdus.length; i++) { encoded[i] = Base64.encodeBytes((byte[]) pdus[i]); } - return dataBuilder.putStringArray(KEY_PDUS, encoded) - .putInt(KEY_SUBSCRIPTION_ID, subscriptionId) - .build(); + return new Data.Builder().putStringArray(KEY_PDUS, encoded) + .putInt(KEY_SUBSCRIPTION_ID, subscriptionId) + .build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -155,6 +145,24 @@ public class SmsReceiveJob extends ContextJob { } private class MigrationPendingException extends Exception { + } + public static final class Factory implements Job.Factory { + @Override + public @NonNull SmsReceiveJob create(@NonNull Parameters parameters, @NonNull Data data) { + try { + int subscriptionId = data.getInt(KEY_SUBSCRIPTION_ID); + String[] encoded = data.getStringArray(KEY_PDUS); + Object[] pdus = new Object[encoded.length]; + + for (int i = 0; i < encoded.length; i++) { + pdus[i] = Base64.decode(encoded[i]); + } + + return new SmsReceiveJob(parameters, pdus, subscriptionId); + } catch (IOException e) { + throw new AssertionError(e); + } + } } } diff --git a/src/org/thoughtcrime/securesms/jobs/SmsSendJob.java b/src/org/thoughtcrime/securesms/jobs/SmsSendJob.java index 90fa3dbd1..0e49d60aa 100644 --- a/src/org/thoughtcrime/securesms/jobs/SmsSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/SmsSendJob.java @@ -9,12 +9,11 @@ import android.support.annotation.NonNull; import android.telephony.PhoneNumberUtils; import android.telephony.SmsManager; -import org.thoughtcrime.securesms.jobmanager.SafeData; -import org.thoughtcrime.securesms.jobs.requirements.NetworkOrServiceRequirement; -import org.thoughtcrime.securesms.jobs.requirements.ServiceRequirement; -import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint; +import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraint; -import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.SmsDatabase; @@ -22,20 +21,16 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.SmsDeliveryListener; -import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.util.NumberUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.jobmanager.JobParameters; import java.util.ArrayList; -import androidx.work.Data; -import androidx.work.WorkerParameters; - public class SmsSendJob extends SendJob { - private static final long serialVersionUID = -5118520036244759718L; + public static final String KEY = "SmsSendJob"; + private static final String TAG = SmsSendJob.class.getSimpleName(); private static final int MAX_ATTEMPTS = 15; private static final String KEY_MESSAGE_ID = "message_id"; @@ -44,44 +39,35 @@ public class SmsSendJob extends SendJob { private long messageId; private int runAttempt; - public SmsSendJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); - } - public SmsSendJob(Context context, long messageId, String name) { this(context, messageId, name, 0); } public SmsSendJob(Context context, long messageId, String name, int runAttempt) { - super(context, constructParameters(name)); + this(constructParameters(context, name), messageId, runAttempt); + } + + private SmsSendJob(@NonNull Job.Parameters parameters, long messageId, int runAttempt) { + super(parameters); + this.messageId = messageId; this.runAttempt = runAttempt; } @Override - protected void initialize(@NonNull SafeData data) { - messageId = data.getLong(KEY_MESSAGE_ID); - runAttempt = data.getInt(KEY_RUN_ATTEMPT); + public @NonNull Data serialize() { + return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId) + .putInt(KEY_RUN_ATTEMPT, runAttempt) + .build(); } @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putLong(KEY_MESSAGE_ID, messageId) - .putInt(KEY_RUN_ATTEMPT, runAttempt) - .build(); + public @NonNull String getFactoryKey() { + return KEY; } @Override - public void onAdded() { - } - - @Override - public void onSend() throws NoSuchMessageException, RequirementNotMetException, TooManyRetriesException { - if (!requirementsMet()) { - warn(TAG, "No service. Retrying."); - throw new RequirementNotMetException(); - } - + public void onSend() throws NoSuchMessageException, TooManyRetriesException { if (runAttempt >= MAX_ATTEMPTS) { warn(TAG, "Hit the retry limit. Failing."); throw new TooManyRetriesException(); @@ -124,14 +110,6 @@ public class SmsSendJob extends SendJob { } } - private boolean requirementsMet() { - if (TextSecurePreferences.isWifiSmsEnabled(context)) { - return new NetworkOrServiceRequirement(context).isPresent(); - } else { - return new ServiceRequirement(context).isPresent(); - } - } - private void deliver(SmsMessageRecord message) throws UndeliverableMessageException { @@ -223,7 +201,7 @@ public class SmsSendJob extends SendJob { pending.putExtra("type", type); pending.putExtra("message_id", messageId); - pending.putExtra("run_attempt", Math.max(runAttempt, getRunAttemptCount())); + pending.putExtra("run_attempt", Math.max(runAttempt, getRunAttempt())); pending.putExtra("upgraded", upgraded); pending.putExtra("push", push); @@ -248,15 +226,22 @@ public class SmsSendJob extends SendJob { } } - private static JobParameters constructParameters(String name) { - JobParameters.Builder builder = JobParameters.newBuilder() - .withRetryCount(MAX_ATTEMPTS) - .withGroupId(name); - return builder.create(); + private static Job.Parameters constructParameters(@NonNull Context context, String name) { + String constraint = TextSecurePreferences.isWifiSmsEnabled(context) ? NetworkOrCellServiceConstraint.KEY + : CellServiceConstraint.KEY; + return new Job.Parameters.Builder() + .setMaxAttempts(MAX_ATTEMPTS) + .setQueue(name) + .addConstraint(constraint) + .build(); } private static class TooManyRetriesException extends Exception { } - private static class RequirementNotMetException extends Exception { } - + public static class Factory implements Job.Factory { + @Override + public @NonNull SmsSendJob create(@NonNull Parameters parameters, @NonNull org.thoughtcrime.securesms.jobmanager.Data data) { + return new SmsSendJob(parameters, data.getLong(KEY_MESSAGE_ID), data.getInt(KEY_RUN_ATTEMPT)); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/SmsSentJob.java b/src/org/thoughtcrime/securesms/jobs/SmsSentJob.java index ba654cf69..fa765b7c9 100644 --- a/src/org/thoughtcrime/securesms/jobs/SmsSentJob.java +++ b/src/org/thoughtcrime/securesms/jobs/SmsSentJob.java @@ -1,30 +1,26 @@ package org.thoughtcrime.securesms.jobs; import android.app.Activity; -import android.content.Context; import android.support.annotation.NonNull; import android.telephony.SmsManager; -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.ApplicationContext; -import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; -import org.thoughtcrime.securesms.jobmanager.JobParameters; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.service.SmsDeliveryListener; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class SmsSentJob extends BaseJob { -public class SmsSentJob extends ContextJob { + public static final String KEY = "SmsSentJob"; - private static final long serialVersionUID = -2624694558755317560L; - private static final String TAG = SmsSentJob.class.getSimpleName(); + private static final String TAG = SmsSentJob.class.getSimpleName(); private static final String KEY_MESSAGE_ID = "message_id"; private static final String KEY_ACTION = "action"; @@ -36,13 +32,16 @@ public class SmsSentJob extends ContextJob { private int result; private int runAttempt; - public SmsSentJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public SmsSentJob(long messageId, String action, int result, int runAttempt) { + this(new Job.Parameters.Builder().build(), + messageId, + action, + result, + runAttempt); } - public SmsSentJob(Context context, long messageId, String action, int result, int runAttempt) { - super(context, JobParameters.newBuilder() - .create()); + private SmsSentJob(@NonNull Job.Parameters parameters, long messageId, String action, int result, int runAttempt) { + super(parameters); this.messageId = messageId; this.action = action; @@ -51,20 +50,17 @@ public class SmsSentJob extends ContextJob { } @Override - protected void initialize(@NonNull SafeData data) { - messageId = data.getLong(KEY_MESSAGE_ID); - action = data.getString(KEY_ACTION); - result = data.getInt(KEY_RESULT); - runAttempt = data.getInt(KEY_RUN_ATTEMPT); + public @NonNull Data serialize() { + return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId) + .putString(KEY_ACTION, action) + .putInt(KEY_RESULT, result) + .putInt(KEY_RUN_ATTEMPT, runAttempt) + .build(); } @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putLong(KEY_MESSAGE_ID, messageId) - .putString(KEY_ACTION, action) - .putInt(KEY_RESULT, result) - .putInt(KEY_RUN_ATTEMPT, runAttempt) - .build(); + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -88,7 +84,6 @@ public class SmsSentJob extends ContextJob { @Override public void onCanceled() { - } private void handleDeliveredResult(long messageId, int result) { @@ -108,8 +103,8 @@ public class SmsSentJob extends ContextJob { case SmsManager.RESULT_ERROR_RADIO_OFF: Log.w(TAG, "Service connectivity problem, requeuing..."); ApplicationContext.getInstance(context) - .getJobManager() - .add(new SmsSendJob(context, messageId, record.getIndividualRecipient().getAddress().serialize(), runAttempt + 1)); + .getJobManager() + .add(new SmsSendJob(context, messageId, record.getIndividualRecipient().getAddress().serialize(), runAttempt + 1)); break; default: database.markAsSentFailed(messageId); @@ -119,4 +114,15 @@ public class SmsSentJob extends ContextJob { Log.w(TAG, e); } } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull SmsSentJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new SmsSentJob(parameters, + data.getLong(KEY_MESSAGE_ID), + data.getString(KEY_ACTION), + data.getInt(KEY_RESULT), + data.getInt(KEY_RUN_ATTEMPT)); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/TrimThreadJob.java b/src/org/thoughtcrime/securesms/jobs/TrimThreadJob.java index 6cc4b3e80..818cbac63 100644 --- a/src/org/thoughtcrime/securesms/jobs/TrimThreadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/TrimThreadJob.java @@ -16,19 +16,17 @@ */ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.database.DatabaseFactory; -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.logging.Log; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class TrimThreadJob extends BaseJob { -public class TrimThreadJob extends ContextJob { + public static final String KEY = "TrimThreadJob"; private static final String TAG = TrimThreadJob.class.getSimpleName(); @@ -36,24 +34,23 @@ public class TrimThreadJob extends ContextJob { private long threadId; - public TrimThreadJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public TrimThreadJob(long threadId) { + this(new Job.Parameters.Builder().setQueue("TrimThreadJob").build(), threadId); } - public TrimThreadJob(Context context, long threadId) { - super(context, JobParameters.newBuilder().withGroupId(TrimThreadJob.class.getSimpleName()).create()); - this.context = context; + private TrimThreadJob(@NonNull Job.Parameters parameters, long threadId) { + super(parameters); this.threadId = threadId; } @Override - protected void initialize(@NonNull SafeData data) { - threadId = data.getLong(KEY_THREAD_ID); + public @NonNull Data serialize() { + return new Data.Builder().putLong(KEY_THREAD_ID, threadId).build(); } @Override - protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putLong(KEY_THREAD_ID, threadId).build(); + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -76,4 +73,11 @@ public class TrimThreadJob extends ContextJob { public void onCanceled() { Log.w(TAG, "Canceling trim attempt: " + threadId); } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull TrimThreadJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new TrimThreadJob(parameters, data.getLong(KEY_THREAD_ID)); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java b/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java index 34c242a56..926fde25e 100644 --- a/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; import android.support.annotation.NonNull; import com.annimon.stream.Stream; @@ -8,8 +7,8 @@ import com.annimon.stream.Stream; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; 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.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.GroupUtil; @@ -23,13 +22,13 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkerParameters; +public class TypingSendJob extends BaseJob implements InjectableType { -public class TypingSendJob extends ContextJob implements InjectableType { + public static final String KEY = "TypingSendJob"; private static final String TAG = TypingSendJob.class.getSimpleName(); @@ -41,32 +40,34 @@ public class TypingSendJob extends ContextJob implements InjectableType { @Inject SignalServiceMessageSender messageSender; - public TypingSendJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public TypingSendJob(long threadId, boolean typing) { + this(new Job.Parameters.Builder() + .setQueue("TYPING_" + threadId) + .setMaxAttempts(1) + .setLifespan(TimeUnit.SECONDS.toMillis(5)) + .build(), + threadId, + typing); } - public TypingSendJob(Context context, long threadId, boolean typing) { - super(context, JobParameters.newBuilder() - .withGroupId("TYPING_" + threadId) - .withRetryCount(1) - .create()); + private TypingSendJob(@NonNull Job.Parameters parameters, long threadId, boolean typing) { + super(parameters); this.threadId = threadId; this.typing = typing; } + @Override - protected void initialize(@NonNull SafeData data) { - this.threadId = data.getLong(KEY_THREAD_ID); - this.typing = data.getBoolean(KEY_TYPING); + public @NonNull Data serialize() { + return new Data.Builder().putLong(KEY_THREAD_ID, threadId) + .putBoolean(KEY_TYPING, typing) + .build(); } - @NonNull @Override - protected Data serialize(@NonNull Data.Builder dataBuilder) { - return dataBuilder.putLong(KEY_THREAD_ID, threadId) - .putBoolean(KEY_TYPING, typing) - .build(); + public @NonNull String getFactoryKey() { + return KEY; } @Override @@ -99,11 +100,18 @@ public class TypingSendJob extends ContextJob implements InjectableType { } @Override - protected void onCanceled() { + public void onCanceled() { } @Override protected boolean onShouldRetry(Exception exception) { return false; } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull TypingSendJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new TypingSendJob(parameters, data.getLong(KEY_THREAD_ID), data.getBoolean(KEY_TYPING)); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/UpdateApkJob.java b/src/org/thoughtcrime/securesms/jobs/UpdateApkJob.java index 2e07392f1..e7d3561c8 100644 --- a/src/org/thoughtcrime/securesms/jobs/UpdateApkJob.java +++ b/src/org/thoughtcrime/securesms/jobs/UpdateApkJob.java @@ -8,17 +8,17 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; -import android.os.Build; 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.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; import com.fasterxml.jackson.annotation.JsonProperty; import org.thoughtcrime.securesms.BuildConfig; -import org.thoughtcrime.securesms.jobmanager.JobParameters; import org.thoughtcrime.securesms.service.UpdateApkReadyListener; import org.thoughtcrime.securesms.util.FileUtils; import org.thoughtcrime.securesms.util.Hex; @@ -29,35 +29,36 @@ import java.io.FileInputStream; import java.io.IOException; import java.security.MessageDigest; -import androidx.work.Data; -import androidx.work.WorkerParameters; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -public class UpdateApkJob extends ContextJob { +public class UpdateApkJob extends BaseJob { + + public static final String KEY = "UpdateApkJob"; private static final String TAG = UpdateApkJob.class.getSimpleName(); - public UpdateApkJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) { - super(context, workerParameters); + public UpdateApkJob() { + this(new Job.Parameters.Builder() + .setQueue("UpdateApkJob") + .addConstraint(NetworkConstraint.KEY) + .setMaxAttempts(2) + .build()); } - public UpdateApkJob(Context context) { - super(context, JobParameters.newBuilder() - .withGroupId(UpdateApkJob.class.getSimpleName()) - .withNetworkRequirement() - .withRetryCount(2) - .create()); + private UpdateApkJob(@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 @@ -260,4 +261,11 @@ public class UpdateApkJob extends ContextJob { return downloadId; } } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull UpdateApkJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new UpdateApkJob(parameters); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirement.java b/src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirement.java deleted file mode 100644 index b33f42461..000000000 --- a/src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirement.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.thoughtcrime.securesms.jobs.requirements; - -import android.content.Context; - -import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent; -import org.thoughtcrime.securesms.jobmanager.requirements.Requirement; -import org.thoughtcrime.securesms.jobmanager.requirements.SimpleRequirement; -import org.thoughtcrime.securesms.service.KeyCachingService; - -public class MasterSecretRequirement extends SimpleRequirement implements ContextDependent { - - private transient Context context; - - public MasterSecretRequirement(Context context) { - this.context = context; - } - - @Override - public boolean isPresent() { - return KeyCachingService.getMasterSecret(context) != null; - } - - @Override - public void setContext(Context context) { - this.context = context; - } -} diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirementProvider.java b/src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirementProvider.java deleted file mode 100644 index db036ff96..000000000 --- a/src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirementProvider.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.thoughtcrime.securesms.jobs.requirements; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; - -import org.thoughtcrime.securesms.jobmanager.requirements.RequirementListener; -import org.thoughtcrime.securesms.jobmanager.requirements.RequirementProvider; -import org.thoughtcrime.securesms.service.KeyCachingService; - -public class MasterSecretRequirementProvider implements RequirementProvider { - - private final BroadcastReceiver newKeyReceiver; - - private RequirementListener listener; - - public MasterSecretRequirementProvider(Context context) { - this.newKeyReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (listener != null) { - listener.onRequirementStatusChanged(); - } - } - }; - - IntentFilter filter = new IntentFilter(KeyCachingService.NEW_KEY_EVENT); - context.registerReceiver(newKeyReceiver, filter, KeyCachingService.KEY_PERMISSION, null); - } - - @Override - public void setListener(RequirementListener listener) { - this.listener = listener; - } -} diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/NetworkOrServiceRequirement.java b/src/org/thoughtcrime/securesms/jobs/requirements/NetworkOrServiceRequirement.java deleted file mode 100644 index 5dcf686b5..000000000 --- a/src/org/thoughtcrime/securesms/jobs/requirements/NetworkOrServiceRequirement.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.thoughtcrime.securesms.jobs.requirements; - -import android.content.Context; - -import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent; -import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement; -import org.thoughtcrime.securesms.jobmanager.requirements.Requirement; -import org.thoughtcrime.securesms.jobmanager.requirements.SimpleRequirement; - -public class NetworkOrServiceRequirement extends SimpleRequirement implements ContextDependent { - - private transient Context context; - - public NetworkOrServiceRequirement(Context context) { - this.context = context; - } - - @Override - public void setContext(Context context) { - this.context = context; - } - - @Override - public boolean isPresent() { - NetworkRequirement networkRequirement = new NetworkRequirement(context); - ServiceRequirement serviceRequirement = new ServiceRequirement(context); - - return networkRequirement.isPresent() || serviceRequirement.isPresent(); - } -} diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirement.java b/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirement.java deleted file mode 100644 index 49a0368c5..000000000 --- a/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirement.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.thoughtcrime.securesms.jobs.requirements; - -import android.content.Context; - -import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent; -import org.thoughtcrime.securesms.jobmanager.requirements.Requirement; -import org.thoughtcrime.securesms.jobmanager.requirements.SimpleRequirement; -import org.thoughtcrime.securesms.sms.TelephonyServiceState; - -public class ServiceRequirement extends SimpleRequirement implements ContextDependent { - - private static final String TAG = ServiceRequirement.class.getSimpleName(); - - private transient Context context; - - public ServiceRequirement(Context context) { - this.context = context; - } - - @Override - public void setContext(Context context) { - this.context = context; - } - - @Override - public boolean isPresent() { - TelephonyServiceState telephonyServiceState = new TelephonyServiceState(); - return telephonyServiceState.isConnected(context); - } -} diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirementProvider.java b/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirementProvider.java deleted file mode 100644 index 4b7ccd08a..000000000 --- a/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirementProvider.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.thoughtcrime.securesms.jobs.requirements; - -import android.content.Context; -import android.telephony.PhoneStateListener; -import android.telephony.ServiceState; -import android.telephony.TelephonyManager; - -import org.thoughtcrime.securesms.jobmanager.requirements.RequirementListener; -import org.thoughtcrime.securesms.jobmanager.requirements.RequirementProvider; - -import java.util.concurrent.atomic.AtomicBoolean; - -public class ServiceRequirementProvider implements RequirementProvider { - - private final TelephonyManager telephonyManager; - private final ServiceStateListener serviceStateListener; - private final AtomicBoolean listeningForServiceState; - - private RequirementListener requirementListener; - - public ServiceRequirementProvider(Context context) { - this.telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - this.serviceStateListener = new ServiceStateListener(); - this.listeningForServiceState = new AtomicBoolean(false); - } - - @Override - public void setListener(RequirementListener requirementListener) { - this.requirementListener = requirementListener; - } - - public void start() { - if (listeningForServiceState.compareAndSet(false, true)) { - this.telephonyManager.listen(serviceStateListener, PhoneStateListener.LISTEN_SERVICE_STATE); - } - } - - private void handleInService() { - if (listeningForServiceState.compareAndSet(true, false)) { - this.telephonyManager.listen(serviceStateListener, PhoneStateListener.LISTEN_NONE); - } - - if (requirementListener != null) { - requirementListener.onRequirementStatusChanged(); - } - } - - private class ServiceStateListener extends PhoneStateListener { - @Override - public void onServiceStateChanged(ServiceState serviceState) { - if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) { - handleInService(); - } - } - } -} diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/SqlCipherMigrationRequirement.java b/src/org/thoughtcrime/securesms/jobs/requirements/SqlCipherMigrationRequirement.java deleted file mode 100644 index 76dd67b66..000000000 --- a/src/org/thoughtcrime/securesms/jobs/requirements/SqlCipherMigrationRequirement.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.thoughtcrime.securesms.jobs.requirements; - - -import android.content.Context; -import android.support.annotation.NonNull; - -import org.thoughtcrime.securesms.jobmanager.dependencies.ContextDependent; -import org.thoughtcrime.securesms.jobmanager.requirements.Requirement; -import org.thoughtcrime.securesms.jobmanager.requirements.SimpleRequirement; -import org.thoughtcrime.securesms.util.TextSecurePreferences; - -public class SqlCipherMigrationRequirement extends SimpleRequirement implements ContextDependent { - - @SuppressWarnings("unused") - private static final String TAG = SqlCipherMigrationRequirement.class.getSimpleName(); - - private transient Context context; - - public SqlCipherMigrationRequirement(@NonNull Context context) { - this.context = context; - } - - @Override - public void setContext(Context context) { - this.context = context; - } - - @Override - public boolean isPresent() { - return !TextSecurePreferences.getNeedsSqlCipherMigration(context); - } -} diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/SqlCipherMigrationRequirementProvider.java b/src/org/thoughtcrime/securesms/jobs/requirements/SqlCipherMigrationRequirementProvider.java deleted file mode 100644 index 2ce574f51..000000000 --- a/src/org/thoughtcrime/securesms/jobs/requirements/SqlCipherMigrationRequirementProvider.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.thoughtcrime.securesms.jobs.requirements; - - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.thoughtcrime.securesms.jobmanager.requirements.RequirementListener; -import org.thoughtcrime.securesms.jobmanager.requirements.RequirementProvider; - -public class SqlCipherMigrationRequirementProvider implements RequirementProvider { - - private RequirementListener listener; - - public SqlCipherMigrationRequirementProvider() { - EventBus.getDefault().register(this); - } - - @Override - public void setListener(RequirementListener listener) { - this.listener = listener; - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEvent(SqlCipherNeedsMigrationEvent event) { - if (listener != null) listener.onRequirementStatusChanged(); - } - - public static class SqlCipherNeedsMigrationEvent { - - } -} diff --git a/src/org/thoughtcrime/securesms/logsubmit/SubmitLogFragment.java b/src/org/thoughtcrime/securesms/logsubmit/SubmitLogFragment.java index 23d49c48b..77bc2da90 100644 --- a/src/org/thoughtcrime/securesms/logsubmit/SubmitLogFragment.java +++ b/src/org/thoughtcrime/securesms/logsubmit/SubmitLogFragment.java @@ -51,7 +51,6 @@ import android.widget.Toast; import org.json.JSONException; import org.json.JSONObject; import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher; import org.thoughtcrime.securesms.logging.Log; @@ -93,6 +92,7 @@ public class SubmitLogFragment extends Fragment { private static final String API_ENDPOINT = "https://debuglogs.org"; private static final String HEADER_SYSINFO = "========== SYSINFO ========"; + private static final String HEADER_JOBS = "=========== JOBS ========="; private static final String HEADER_LOGCAT = "========== LOGCAT ========"; private static final String HEADER_LOGGER = "========== LOGGER ========"; @@ -373,6 +373,8 @@ public class SubmitLogFragment extends Fragment { return HEADER_SYSINFO + "\n\n" + buildDescription(context) + "\n\n\n" + + HEADER_JOBS + "\n\n" + + scrubber.scrub(ApplicationContext.getInstance(context).getJobManager().getDebugInfo()) + "\n\n" + HEADER_LOGCAT + "\n\n" + scrubbedLogcat + "\n\n\n" + HEADER_LOGGER + "\n\n" + @@ -487,7 +489,7 @@ public class SubmitLogFragment extends Fragment { final PackageManager pm = context.getPackageManager(); final StringBuilder builder = new StringBuilder(); - + builder.append("Time : ").append(System.currentTimeMillis()).append('\n'); builder.append("Device : ") .append(Build.MANUFACTURER).append(" ") .append(Build.MODEL).append(" (") diff --git a/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index 718292179..670fe42ce 100644 --- a/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -77,7 +77,7 @@ public class MarkReadReceiver extends BroadcastReceiver { ApplicationContext.getInstance(context) .getJobManager() - .add(new MultiDeviceReadUpdateJob(context, syncMessageIds)); + .add(new MultiDeviceReadUpdateJob(syncMessageIds)); Map> addressMap = Stream.of(markedReadMessages) .map(MarkedMessageInfo::getSyncMessageId) @@ -88,7 +88,7 @@ public class MarkReadReceiver extends BroadcastReceiver { ApplicationContext.getInstance(context) .getJobManager() - .add(new SendReadReceiptJob(context, address, timestamps)); + .add(new SendReadReceiptJob(address, timestamps)); } } diff --git a/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java index 90eea15b4..aa85a68d9 100644 --- a/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java @@ -4,13 +4,11 @@ import android.app.Activity; import android.app.KeyguardManager; import android.content.Context; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; import android.support.v7.preference.CheckBoxPreference; import android.support.v7.preference.Preference; -import android.view.View; import android.widget.Toast; import org.thoughtcrime.securesms.ApplicationContext; @@ -22,7 +20,6 @@ import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob; -import org.thoughtcrime.securesms.jobs.MultiDeviceReadReceiptUpdateJob; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.lock.RegistrationLockDialog; import org.thoughtcrime.securesms.service.KeyCachingService; @@ -187,8 +184,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment boolean enabled = (boolean)newValue; ApplicationContext.getInstance(getContext()) .getJobManager() - .add(new MultiDeviceConfigurationUpdateJob(getContext(), - enabled, + .add(new MultiDeviceConfigurationUpdateJob(enabled, TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()), TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()), TextSecurePreferences.isLinkPreviewsEnabled(getContext()))); @@ -203,8 +199,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment boolean enabled = (boolean)newValue; ApplicationContext.getInstance(getContext()) .getJobManager() - .add(new MultiDeviceConfigurationUpdateJob(getContext(), - TextSecurePreferences.isReadReceiptsEnabled(requireContext()), + .add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()), enabled, TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()), TextSecurePreferences.isLinkPreviewsEnabled(getContext()))); @@ -223,8 +218,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment boolean enabled = (boolean)newValue; ApplicationContext.getInstance(requireContext()) .getJobManager() - .add(new MultiDeviceConfigurationUpdateJob(requireContext(), - TextSecurePreferences.isReadReceiptsEnabled(requireContext()), + .add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()), TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()), TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(requireContext()), enabled)); @@ -327,8 +321,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment boolean enabled = (boolean) newValue; ApplicationContext.getInstance(getContext()) .getJobManager() - .add(new MultiDeviceConfigurationUpdateJob(getContext(), - TextSecurePreferences.isReadReceiptsEnabled(getContext()), + .add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(getContext()), TextSecurePreferences.isTypingIndicatorsEnabled(getContext()), enabled, TextSecurePreferences.isLinkPreviewsEnabled(getContext()))); @@ -342,7 +335,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment public boolean onPreferenceChange(Preference preference, Object o) { ApplicationContext.getInstance(getContext()) .getJobManager() - .add(new RefreshAttributesJob(getContext())); + .add(new RefreshAttributesJob()); return true; } } diff --git a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java index 3b950703b..05d4999a4 100644 --- a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java @@ -163,7 +163,7 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { Log.i(TAG, "Queing backup..."); ApplicationContext.getInstance(getContext()) .getJobManager() - .add(new LocalBackupJob(getContext())); + .add(new LocalBackupJob()); }) .withPermanentDenialDialog(getString(R.string.ChatsPreferenceFragment_signal_requires_external_storage_permission_in_order_to_create_backups)) .execute(); diff --git a/src/org/thoughtcrime/securesms/service/BootReceiver.java b/src/org/thoughtcrime/securesms/service/BootReceiver.java index e94765b44..7435a6ace 100644 --- a/src/org/thoughtcrime/securesms/service/BootReceiver.java +++ b/src/org/thoughtcrime/securesms/service/BootReceiver.java @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.os.Build; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; diff --git a/src/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java b/src/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java index d31aff40d..8abacdca4 100644 --- a/src/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java +++ b/src/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java @@ -24,7 +24,7 @@ public class DirectoryRefreshListener extends PersistentAlarmManagerListener { if (scheduledTime != 0 && TextSecurePreferences.isPushRegistered(context)) { ApplicationContext.getInstance(context) .getJobManager() - .add(new DirectoryRefreshJob(context, true)); + .add(new DirectoryRefreshJob(true)); } long newTime = System.currentTimeMillis() + INTERVAL; diff --git a/src/org/thoughtcrime/securesms/service/IncomingMessageObserver.java b/src/org/thoughtcrime/securesms/service/IncomingMessageObserver.java index dacbc49e6..c0d7ac95a 100644 --- a/src/org/thoughtcrime/securesms/service/IncomingMessageObserver.java +++ b/src/org/thoughtcrime/securesms/service/IncomingMessageObserver.java @@ -12,14 +12,14 @@ import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.support.v4.content.ContextCompat; +import org.thoughtcrime.securesms.jobmanager.ConstraintObserver; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver; import org.thoughtcrime.securesms.logging.Log; 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.requirements.NetworkRequirementProvider; -import org.thoughtcrime.securesms.jobmanager.requirements.RequirementListener; import org.thoughtcrime.securesms.jobs.PushContentReceiveJob; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; @@ -33,7 +33,7 @@ import java.util.concurrent.TimeoutException; import javax.inject.Inject; -public class IncomingMessageObserver implements InjectableType, RequirementListener { +public class IncomingMessageObserver implements InjectableType, ConstraintObserver.Notifier { private static final String TAG = IncomingMessageObserver.class.getSimpleName(); @@ -43,8 +43,8 @@ public class IncomingMessageObserver implements InjectableType, RequirementListe private static SignalServiceMessagePipe pipe = null; private static SignalServiceMessagePipe unidentifiedPipe = null; - private final Context context; - private final NetworkRequirement networkRequirement; + private final Context context; + private final NetworkConstraint networkConstraint; private boolean appVisible; @@ -55,9 +55,9 @@ public class IncomingMessageObserver implements InjectableType, RequirementListe ApplicationContext.getInstance(context).injectDependencies(this); this.context = context; - this.networkRequirement = new NetworkRequirement(context); + this.networkConstraint = new NetworkConstraint.Factory(ApplicationContext.getInstance(context)).create(); - new NetworkRequirementProvider(context).setListener(this); + new NetworkConstraintObserver(ApplicationContext.getInstance(context)).register(this); new MessageRetrievalThread().start(); if (TextSecurePreferences.isFcmDisabled(context)) { @@ -78,7 +78,7 @@ public class IncomingMessageObserver implements InjectableType, RequirementListe } @Override - public void onRequirementStatusChanged() { + public void onConstraintMet(@NonNull String reason) { synchronized (this) { notifyAll(); } @@ -98,12 +98,12 @@ public class IncomingMessageObserver implements InjectableType, RequirementListe boolean isGcmDisabled = TextSecurePreferences.isFcmDisabled(context); Log.d(TAG, String.format("Network requirement: %s, app visible: %s, gcm disabled: %b", - networkRequirement.isPresent(), appVisible, isGcmDisabled)); + networkConstraint.isMet(), appVisible, isGcmDisabled)); return TextSecurePreferences.isPushRegistered(context) && TextSecurePreferences.isWebsocketRegistered(context) && (appVisible || isGcmDisabled) && - networkRequirement.isPresent() && + networkConstraint.isMet() && !networkAccess.isCensored(context); } diff --git a/src/org/thoughtcrime/securesms/service/LocalBackupListener.java b/src/org/thoughtcrime/securesms/service/LocalBackupListener.java index d60dad349..57ae5de31 100644 --- a/src/org/thoughtcrime/securesms/service/LocalBackupListener.java +++ b/src/org/thoughtcrime/securesms/service/LocalBackupListener.java @@ -22,7 +22,7 @@ public class LocalBackupListener extends PersistentAlarmManagerListener { @Override protected long onAlarm(Context context, long scheduledTime) { if (TextSecurePreferences.isBackupEnabled(context)) { - ApplicationContext.getInstance(context).getJobManager().add(new LocalBackupJob(context)); + ApplicationContext.getInstance(context).getJobManager().add(new LocalBackupJob()); } long nextTime = System.currentTimeMillis() + INTERVAL; diff --git a/src/org/thoughtcrime/securesms/service/MmsListener.java b/src/org/thoughtcrime/securesms/service/MmsListener.java index 09d9797cb..b7d3d9338 100644 --- a/src/org/thoughtcrime/securesms/service/MmsListener.java +++ b/src/org/thoughtcrime/securesms/service/MmsListener.java @@ -19,13 +19,11 @@ package org.thoughtcrime.securesms.service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.os.Build; import android.provider.Telephony; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.jobs.MmsReceiveJob; -import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; public class MmsListener extends BroadcastReceiver { @@ -58,7 +56,7 @@ public class MmsListener extends BroadcastReceiver { ApplicationContext.getInstance(context) .getJobManager() - .add(new MmsReceiveJob(context, intent.getByteArrayExtra("data"), subscriptionId)); + .add(new MmsReceiveJob(intent.getByteArrayExtra("data"), subscriptionId)); abortBroadcast(); } diff --git a/src/org/thoughtcrime/securesms/service/RotateSignedPreKeyListener.java b/src/org/thoughtcrime/securesms/service/RotateSignedPreKeyListener.java index 0eee795d6..06ed207f2 100644 --- a/src/org/thoughtcrime/securesms/service/RotateSignedPreKeyListener.java +++ b/src/org/thoughtcrime/securesms/service/RotateSignedPreKeyListener.java @@ -24,7 +24,7 @@ public class RotateSignedPreKeyListener extends PersistentAlarmManagerListener { if (scheduledTime != 0 && TextSecurePreferences.isPushRegistered(context)) { ApplicationContext.getInstance(context) .getJobManager() - .add(new RotateSignedPreKeyJob(context)); + .add(new RotateSignedPreKeyJob()); } long nextTime = System.currentTimeMillis() + INTERVAL; diff --git a/src/org/thoughtcrime/securesms/service/SmsDeliveryListener.java b/src/org/thoughtcrime/securesms/service/SmsDeliveryListener.java index 09047f261..00854e0f8 100644 --- a/src/org/thoughtcrime/securesms/service/SmsDeliveryListener.java +++ b/src/org/thoughtcrime/securesms/service/SmsDeliveryListener.java @@ -4,11 +4,12 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.telephony.SmsMessage; + +import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.database.SmsDatabase; -import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobs.SmsSentJob; public class SmsDeliveryListener extends BroadcastReceiver { @@ -20,15 +21,15 @@ public class SmsDeliveryListener extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); - long messageId = intent.getLongExtra("message_id", -1); - int runAttempt = intent.getIntExtra("run_attempt", 0); + JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); + long messageId = intent.getLongExtra("message_id", -1); + int runAttempt = intent.getIntExtra("run_attempt", 0); switch (intent.getAction()) { case SENT_SMS_ACTION: int result = getResultCode(); - jobManager.add(new SmsSentJob(context, messageId, SENT_SMS_ACTION, result, runAttempt)); + jobManager.add(new SmsSentJob(messageId, SENT_SMS_ACTION, result, runAttempt)); break; case DELIVERED_SMS_ACTION: byte[] pdu = intent.getByteArrayExtra("pdu"); @@ -59,7 +60,7 @@ public class SmsDeliveryListener extends BroadcastReceiver { else if (status >> 24 == 3) status = SmsDatabase.Status.STATUS_FAILED; } - jobManager.add(new SmsSentJob(context, messageId, DELIVERED_SMS_ACTION, status, runAttempt)); + jobManager.add(new SmsSentJob(messageId, DELIVERED_SMS_ACTION, status, runAttempt)); break; default: Log.w(TAG, "Unknown action: " + intent.getAction()); diff --git a/src/org/thoughtcrime/securesms/service/SmsListener.java b/src/org/thoughtcrime/securesms/service/SmsListener.java index 67bf62c45..28df7b3bb 100644 --- a/src/org/thoughtcrime/securesms/service/SmsListener.java +++ b/src/org/thoughtcrime/securesms/service/SmsListener.java @@ -19,7 +19,6 @@ package org.thoughtcrime.securesms.service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import android.provider.Telephony; import android.telephony.SmsMessage; @@ -27,7 +26,6 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.jobs.SmsReceiveJob; -import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; public class SmsListener extends BroadcastReceiver { @@ -107,7 +105,7 @@ public class SmsListener extends BroadcastReceiver { Object[] pdus = (Object[]) intent.getExtras().get("pdus"); int subscriptionId = intent.getExtras().getInt("subscription", -1); - ApplicationContext.getInstance(context).getJobManager().add(new SmsReceiveJob(context, pdus, subscriptionId)); + ApplicationContext.getInstance(context).getJobManager().add(new SmsReceiveJob(pdus, subscriptionId)); abortBroadcast(); } diff --git a/src/org/thoughtcrime/securesms/service/UpdateApkRefreshListener.java b/src/org/thoughtcrime/securesms/service/UpdateApkRefreshListener.java index 4d4d7bdbd..cebf3499f 100644 --- a/src/org/thoughtcrime/securesms/service/UpdateApkRefreshListener.java +++ b/src/org/thoughtcrime/securesms/service/UpdateApkRefreshListener.java @@ -31,7 +31,7 @@ public class UpdateApkRefreshListener extends PersistentAlarmManagerListener { Log.i(TAG, "Queueing APK update job..."); ApplicationContext.getInstance(context) .getJobManager() - .add(new UpdateApkJob(context)); + .add(new UpdateApkJob()); } long newTime = System.currentTimeMillis() + INTERVAL; diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index c017188fd..ea0a27bbb 100644 --- a/src/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java @@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; +import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.ApplicationContext; @@ -35,7 +36,6 @@ import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobs.MmsSendJob; import org.thoughtcrime.securesms.jobs.PushGroupSendJob; import org.thoughtcrime.securesms.jobs.PushMediaSendJob; @@ -159,7 +159,7 @@ public class MessageSender { private static void sendTextPush(Context context, Recipient recipient, long messageId) { JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); - jobManager.add(new PushTextSendJob(context, messageId, recipient.getAddress())); + jobManager.add(new PushTextSendJob(messageId, recipient.getAddress())); } private static void sendMediaPush(Context context, Recipient recipient, long messageId) { @@ -179,7 +179,7 @@ public class MessageSender { private static void sendMms(Context context, long messageId) { JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); - jobManager.add(new MmsSendJob(context, messageId)); + jobManager.add(new MmsSendJob(messageId)); } private static boolean isPushTextSend(Context context, Recipient recipient, boolean keyExchange) { diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index c8a015446..cd769178e 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -11,11 +11,12 @@ import android.support.annotation.ArrayRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; + +import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver; import org.thoughtcrime.securesms.logging.Log; import org.greenrobot.eventbus.EventBus; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.jobs.requirements.SqlCipherMigrationRequirementProvider; import org.thoughtcrime.securesms.lock.RegistrationLockReminders; import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; import org.whispersystems.libsignal.util.Medium; @@ -285,7 +286,7 @@ public class TextSecurePreferences { public static void setNeedsSqlCipherMigration(@NonNull Context context, boolean value) { setBooleanPreference(context, NEEDS_SQLCIPHER_MIGRATION, value); - EventBus.getDefault().post(new SqlCipherMigrationRequirementProvider.SqlCipherNeedsMigrationEvent()); + EventBus.getDefault().post(new SqlCipherMigrationConstraintObserver.SqlCipherNeedsMigrationEvent()); } public static boolean getNeedsSqlCipherMigration(@NonNull Context context) { diff --git a/test/unitTest/java/org/thoughtcrime/securesms/jobmanager/impl/JsonDataSerializerTest.java b/test/unitTest/java/org/thoughtcrime/securesms/jobmanager/impl/JsonDataSerializerTest.java new file mode 100644 index 000000000..cd2a66e62 --- /dev/null +++ b/test/unitTest/java/org/thoughtcrime/securesms/jobmanager/impl/JsonDataSerializerTest.java @@ -0,0 +1,47 @@ +package org.thoughtcrime.securesms.jobmanager.impl; + +import org.junit.Test; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.util.Util; + +import java.io.IOException; + +import static org.junit.Assert.*; + +public final class JsonDataSerializerTest { + + private static final float FloatDelta = 0.00001f; + + @Test + public void deserialize_dataMatchesExpected() throws IOException { + Data data = new JsonDataSerializer().deserialize(Util.readFullyAsString(ClassLoader.getSystemClassLoader().getResourceAsStream("data/data_serialized.json"))); + + assertEquals("s1 value", data.getString("s1")); + assertEquals("s2 value", data.getString("s2")); + assertArrayEquals(new String[]{ "a", "b", "c" }, data.getStringArray("s_array_1")); + + assertEquals(1, data.getInt("i1")); + assertEquals(2, data.getInt("i2")); + assertEquals(Integer.MAX_VALUE, data.getInt("max")); + assertEquals(Integer.MIN_VALUE, data.getInt("min")); + assertArrayEquals(new int[]{ 1, 2, 3, Integer.MAX_VALUE, Integer.MIN_VALUE }, data.getIntegerArray("i_array_1")); + + assertEquals(10, data.getLong("l1")); + assertEquals(20, data.getLong("l2")); + assertEquals(Long.MAX_VALUE, data.getLong("max")); + assertEquals(Long.MIN_VALUE, data.getLong("min")); + assertArrayEquals(new long[]{ 1, 2, 3, Long.MAX_VALUE, Long.MIN_VALUE }, data.getLongArray("l_array_1")); + + assertEquals(1.2f, data.getFloat("f1"), FloatDelta); + assertEquals(3.4f, data.getFloat("f2"), FloatDelta); + assertArrayEquals(new float[]{ 5.6f, 7.8f }, data.getFloatArray("f_array_1"), FloatDelta); + + assertEquals(10.2, data.getDouble("d1"), FloatDelta); + assertEquals(30.4, data.getDouble("d2"), FloatDelta); + assertArrayEquals(new double[]{ 50.6, 70.8 }, data.getDoubleArray("d_array_1"), FloatDelta); + + assertTrue(data.getBoolean("b1")); + assertFalse(data.getBoolean("b2")); + assertArrayEquals(new boolean[]{ false, true }, data.getBooleanArray("b_array_1")); + } +} \ No newline at end of file diff --git a/test/unitTest/java/org/thoughtcrime/securesms/jobs/FastJobStorageTest.java b/test/unitTest/java/org/thoughtcrime/securesms/jobs/FastJobStorageTest.java new file mode 100644 index 000000000..7844aa067 --- /dev/null +++ b/test/unitTest/java/org/thoughtcrime/securesms/jobs/FastJobStorageTest.java @@ -0,0 +1,352 @@ +package org.thoughtcrime.securesms.jobs; + +import android.support.annotation.NonNull; + +import com.annimon.stream.Stream; + +import org.junit.Test; +import org.thoughtcrime.securesms.database.JobDatabase; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer; +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.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class FastJobStorageTest { + + private static final JsonDataSerializer serializer = new JsonDataSerializer(); + private static final String EMPTY_DATA = serializer.serialize(Data.EMPTY); + + @Test + public void init_allStoredDataAvailable() { + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)); + + subject.init(); + + DataSet1.assertJobsMatch(subject.getAllJobSpecs()); + DataSet1.assertConstraintsMatch(subject.getAllConstraintSpecs()); + DataSet1.assertDependenciesMatch(subject.getAllDependencySpecs()); + } + + @Test + public void insertJobs_writesToDatabase() { + JobDatabase database = noopDatabase(); + FastJobStorage subject = new FastJobStorage(database); + + subject.insertJobs(DataSet1.FULL_SPECS); + + verify(database).insertJobs(DataSet1.FULL_SPECS); + } + + @Test + public void insertJobs_dataCanBeFound() { + FastJobStorage subject = new FastJobStorage(noopDatabase()); + + subject.insertJobs(DataSet1.FULL_SPECS); + + DataSet1.assertJobsMatch(subject.getAllJobSpecs()); + DataSet1.assertConstraintsMatch(subject.getAllConstraintSpecs()); + DataSet1.assertDependenciesMatch(subject.getAllDependencySpecs()); + } + + @Test + public void insertJobs_individualJobCanBeFound() { + FastJobStorage subject = new FastJobStorage(noopDatabase()); + + subject.insertJobs(DataSet1.FULL_SPECS); + + assertEquals(DataSet1.JOB_1, subject.getJobSpec(DataSet1.JOB_1.getId())); + assertEquals(DataSet1.JOB_2, subject.getJobSpec(DataSet1.JOB_2.getId())); + } + + @Test + public void updateAllJobsToBePending_writesToDatabase() { + JobDatabase database = noopDatabase(); + FastJobStorage subject = new FastJobStorage(database); + + subject.updateAllJobsToBePending(); + + verify(database).updateAllJobsToBePending(); + } + + @Test + public void updateAllJobsToBePending_allArePending() { + FullSpec fullSpec1 = new FullSpec(new JobSpec("1", "f1", null, 1, 1, 1, 1, 1, 1, 1, EMPTY_DATA, true), + Collections.emptyList(), + Collections.emptyList()); + FullSpec fullSpec2 = new FullSpec(new JobSpec("2", "f2", null, 1, 1, 1, 1, 1, 1, 1, EMPTY_DATA, true), + Collections.emptyList(), + Collections.emptyList()); + + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2))); + + subject.init(); + subject.updateAllJobsToBePending(); + + assertFalse(subject.getJobSpec("1").isRunning()); + assertFalse(subject.getJobSpec("2").isRunning()); + } + + @Test + public void updateJobRunningState_writesToDatabase() { + JobDatabase database = noopDatabase(); + FastJobStorage subject = new FastJobStorage(database); + + subject.updateJobRunningState("1", true); + + verify(database).updateJobRunningState("1", true); + } + + @Test + public void updateJobRunningState_stateUpdated() { + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)); + subject.init(); + + subject.updateJobRunningState(DataSet1.JOB_1.getId(), true); + assertTrue(subject.getJobSpec(DataSet1.JOB_1.getId()).isRunning()); + + subject.updateJobRunningState(DataSet1.JOB_1.getId(), false); + assertFalse(subject.getJobSpec(DataSet1.JOB_1.getId()).isRunning()); + } + + @Test + public void updateJobAfterRetry_writesToDatabase() { + JobDatabase database = noopDatabase(); + FastJobStorage subject = new FastJobStorage(database); + + subject.updateJobAfterRetry("1", true, 1, 10); + + verify(database).updateJobAfterRetry("1", true, 1, 10); + } + + @Test + public void updateJobAfterRetry_stateUpdated() { + FullSpec fullSpec = new FullSpec(new JobSpec("1", "f1", null, 0, 0, 0, 3, 30000, -1, -1, EMPTY_DATA, true), + Collections.emptyList(), + Collections.emptyList()); + + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Collections.singletonList(fullSpec))); + + subject.init(); + subject.updateJobAfterRetry("1", false, 1, 10); + + JobSpec job = subject.getJobSpec("1"); + + assertNotNull(job); + assertFalse(job.isRunning()); + assertEquals(1, job.getRunAttempt()); + assertEquals(10, job.getNextRunAttemptTime()); + } + + @Test + public void getPendingJobsWithNoDependenciesInCreatedOrder_noneWhenEarlierItemInQueueInRunning() { + FullSpec fullSpec1 = new FullSpec(new JobSpec("1", "f1", "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true), + Collections.emptyList(), + Collections.emptyList()); + FullSpec fullSpec2 = new FullSpec(new JobSpec("2", "f2", "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.emptyList()); + + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2))); + subject.init(); + + assertEquals(0, subject.getPendingJobsWithNoDependenciesInCreatedOrder(1).size()); + } + + @Test + public void getPendingJobsWithNoDependenciesInCreatedOrder_noneWhenAllJobsAreRunning() { + FullSpec fullSpec = new FullSpec(new JobSpec("1", "f1", "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true), + Collections.emptyList(), + Collections.emptyList()); + + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Collections.singletonList(fullSpec))); + subject.init(); + + assertEquals(0, subject.getPendingJobsWithNoDependenciesInCreatedOrder(10).size()); + } + + @Test + public void getPendingJobsWithNoDependenciesInCreatedOrder_noneWhenNextRunTimeIsAfterCurrentTime() { + FullSpec fullSpec = new FullSpec(new JobSpec("1", "f1", "q", 0, 10, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.emptyList()); + + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Collections.singletonList(fullSpec))); + subject.init(); + + assertEquals(0, subject.getPendingJobsWithNoDependenciesInCreatedOrder(0).size()); + } + + @Test + public void getPendingJobsWithNoDependenciesInCreatedOrder_noneWhenDependentOnAnotherJob() { + FullSpec fullSpec1 = new FullSpec(new JobSpec("1", "f1", null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true), + Collections.emptyList(), + Collections.emptyList()); + FullSpec fullSpec2 = new FullSpec(new JobSpec("2", "f2", null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.singletonList(new DependencySpec("2", "1"))); + + + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2))); + subject.init(); + + assertEquals(0, subject.getPendingJobsWithNoDependenciesInCreatedOrder(0).size()); + } + + @Test + public void getPendingJobsWithNoDependenciesInCreatedOrder_singleEligibleJob() { + FullSpec fullSpec = new FullSpec(new JobSpec("1", "f1", "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.emptyList()); + + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Collections.singletonList(fullSpec))); + subject.init(); + + assertEquals(1, subject.getPendingJobsWithNoDependenciesInCreatedOrder(10).size()); + } + + @Test + public void getPendingJobsWithNoDependenciesInCreatedOrder_multipleEligibleJobs() { + FullSpec fullSpec1 = new FullSpec(new JobSpec("1", "f1", null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.emptyList()); + FullSpec fullSpec2 = new FullSpec(new JobSpec("2", "f2", null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.emptyList()); + + + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2))); + subject.init(); + + assertEquals(2, subject.getPendingJobsWithNoDependenciesInCreatedOrder(10).size()); + } + + @Test + public void getPendingJobsWithNoDependenciesInCreatedOrder_singleEligibleJobInMixedList() { + FullSpec fullSpec1 = new FullSpec(new JobSpec("1", "f1", null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true), + Collections.emptyList(), + Collections.emptyList()); + FullSpec fullSpec2 = new FullSpec(new JobSpec("2", "f2", null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.emptyList()); + + + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2))); + subject.init(); + + List jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10); + + assertEquals(1, jobs.size()); + assertEquals("2", jobs.get(0).getId()); + } + + @Test + public void getPendingJobsWithNoDependenciesInCreatedOrder_firstItemInQueue() { + FullSpec fullSpec1 = new FullSpec(new JobSpec("1", "f1", "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.emptyList()); + FullSpec fullSpec2 = new FullSpec(new JobSpec("2", "f2", "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.emptyList()); + + + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2))); + subject.init(); + + List jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10); + + assertEquals(1, jobs.size()); + assertEquals("1", jobs.get(0).getId()); + } + + @Test + public void deleteJobs_writesToDatabase() { + JobDatabase database = noopDatabase(); + FastJobStorage subject = new FastJobStorage(database); + List ids = Arrays.asList("1", "2"); + + subject.deleteJobs(ids); + + verify(database).deleteJobs(ids); + } + + @Test + public void deleteJobs_deletesAllRelevantPieces() { + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)); + + subject.init(); + subject.deleteJobs(Collections.singletonList("id1")); + + List jobs = subject.getAllJobSpecs(); + List constraints = subject.getAllConstraintSpecs(); + List dependencies = subject.getAllDependencySpecs(); + + assertEquals(1, jobs.size()); + assertEquals(DataSet1.JOB_2, jobs.get(0)); + assertEquals(1, constraints.size()); + assertEquals(DataSet1.CONSTRAINT_2, constraints.get(0)); + assertEquals(0, dependencies.size()); + } + + + private JobDatabase noopDatabase() { + JobDatabase database = mock(JobDatabase.class); + + when(database.getAllJobSpecs()).thenReturn(Collections.emptyList()); + when(database.getAllConstraintSpecs()).thenReturn(Collections.emptyList()); + when(database.getAllDependencySpecs()).thenReturn(Collections.emptyList()); + + return database; + } + + private JobDatabase fixedDataDatabase(List fullSpecs) { + JobDatabase database = mock(JobDatabase.class); + + when(database.getAllJobSpecs()).thenReturn(Stream.of(fullSpecs).map(FullSpec::getJobSpec).toList()); + when(database.getAllConstraintSpecs()).thenReturn(Stream.of(fullSpecs).map(FullSpec::getConstraintSpecs).flatMap(Stream::of).toList()); + when(database.getAllDependencySpecs()).thenReturn(Stream.of(fullSpecs).map(FullSpec::getDependencySpecs).flatMap(Stream::of).toList()); + + return database; + } + + private static final class DataSet1 { + static final JobSpec JOB_1 = new JobSpec("id1", "f1", "q1", 1, 2, 3, 4, 5, 6, 7, EMPTY_DATA, false); + static final JobSpec JOB_2 = new JobSpec("id2", "f2", "q2", 1, 2, 3, 4, 5, 6, 7, EMPTY_DATA, false); + static final ConstraintSpec CONSTRAINT_1 = new ConstraintSpec("id1", "f1"); + static final ConstraintSpec CONSTRAINT_2 = new ConstraintSpec("id2", "f2"); + static final DependencySpec DEPENDENCY_2 = new DependencySpec("id2", "id1"); + static final FullSpec FULL_SPEC_1 = new FullSpec(JOB_1, Collections.singletonList(CONSTRAINT_1), Collections.emptyList()); + static final FullSpec FULL_SPEC_2 = new FullSpec(JOB_2, Collections.singletonList(CONSTRAINT_2), Collections.singletonList(DEPENDENCY_2)); + static final List FULL_SPECS = Arrays.asList(FULL_SPEC_1, FULL_SPEC_2); + + static void assertJobsMatch(@NonNull List jobs) { + assertEquals(jobs.size(), 2); + assertTrue(jobs.contains(DataSet1.JOB_1)); + assertTrue(jobs.contains(DataSet1.JOB_1)); + } + + static void assertConstraintsMatch(@NonNull List constraints) { + assertEquals(constraints.size(), 2); + assertTrue(constraints.contains(DataSet1.CONSTRAINT_1)); + assertTrue(constraints.contains(DataSet1.CONSTRAINT_2)); + } + + static void assertDependenciesMatch(@NonNull List dependencies) { + assertEquals(dependencies.size(), 1); + assertTrue(dependencies.contains(DataSet1.DEPENDENCY_2)); + } + } +} diff --git a/test/unitTest/java/org/thoughtcrime/securesms/testutil/DirectExecutor.java b/test/unitTest/java/org/thoughtcrime/securesms/testutil/DirectExecutor.java new file mode 100644 index 000000000..b602ba25b --- /dev/null +++ b/test/unitTest/java/org/thoughtcrime/securesms/testutil/DirectExecutor.java @@ -0,0 +1,16 @@ +package org.thoughtcrime.securesms.testutil; + +import android.support.annotation.NonNull; + +import java.util.concurrent.Executor; + +/** + * Executor that runs runnables on the same thread {@link #execute(Runnable)} is invoked on. + * Only intended to be used for tests. + */ +public class DirectExecutor implements Executor { + @Override + public void execute(@NonNull Runnable runnable) { + runnable.run(); + } +} diff --git a/test/unitTest/resources/data/data_serialized.json b/test/unitTest/resources/data/data_serialized.json new file mode 100644 index 000000000..7f8d23593 --- /dev/null +++ b/test/unitTest/resources/data/data_serialized.json @@ -0,0 +1,73 @@ +{ + "strings": { + "s1": "s1 value", + "s2": "s2 value" + }, + "stringArrays": { + "s_array_1": [ + "a", + "b", + "c" + ] + }, + "integers": { + "i1": 1, + "i2": 2, + "max": 2147483647, + "min": -2147483648 + }, + "integerArrays": { + "i_array_1": [ + 1, + 2, + 3, + 2147483647, + -2147483648 + ] + }, + "longs": { + "l1": 10, + "l2": 20, + "max": 9223372036854775807, + "min": -9223372036854775808 + }, + "longArrays": { + "l_array_1": [ + 1, + 2, + 3, + 9223372036854775807, + -9223372036854775808 + ] + }, + "floats": { + "f1": 1.2, + "f2": 3.4 + }, + "floatArrays": { + "f_array_1": [ + 5.6, + 7.8 + ] + }, + "doubles": { + "d1": 10.2, + "d2": 30.4 + }, + "doubleArrays": { + "d_array_1": [ + 50.6, + 70.8 + ] + }, + "booleans": { + "b1": true, + "b2": false + }, + "booleanArrays": { + "b_array_1": [ + false, + true + ] + } +} \ No newline at end of file