diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 9b0d5f7750..75cd3b706f 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -261,7 +261,7 @@ android:launchMode="singleTask" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> - diff --git a/res/layout/database_upgrade_activity.xml b/res/layout/application_migration_activity.xml similarity index 95% rename from res/layout/database_upgrade_activity.xml rename to res/layout/application_migration_activity.xml index 56fc4575ac..37e178a620 100644 --- a/res/layout/database_upgrade_activity.xml +++ b/res/layout/application_migration_activity.xml @@ -25,7 +25,7 @@ android:layout_marginBottom="16dip" android:layout_marginTop="16dip" android:gravity="center" - android:text="@string/database_upgrade_activity__updating_database"/> + android:text="@string/ApplicationMigrationActivity__signal_is_updating"/> \+%d + + Signal is updating... + Currently: %s You haven\'t set a passphrase yet! @@ -1113,8 +1116,6 @@ This could take a moment. Please be patient, we\'ll notify you when the import is complete. IMPORTING - - Updating database... Import system SMS database Import the database from the default system messenger app diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 8cd621f99f..9ecaca3668 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -53,6 +53,7 @@ import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger; +import org.thoughtcrime.securesms.migrations.ApplicationMigrations; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.providers.BlobProvider; @@ -114,6 +115,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi initializeCrashHandling(); initializeAppDependencies(); initializeJobManager(); + initializeApplicationMigrations(); initializeMessageRetrieval(); initializeExpiringMessageManager(); initializeRevealableMessageManager(); @@ -130,6 +132,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi initializeCameraX(); NotificationChannels.create(this); ProcessLifecycleOwner.get().getLifecycle().addObserver(this); + jobManager.beginJobLoop(); } @Override @@ -223,6 +226,10 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi .build()); } + private void initializeApplicationMigrations() { + ApplicationMigrations.onApplicationCreate(this, jobManager); + } + public void initializeMessageRetrieval() { this.incomingMessageObserver = new IncomingMessageObserver(this); } diff --git a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java deleted file mode 100644 index a82ef49723..0000000000 --- a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java +++ /dev/null @@ -1,445 +0,0 @@ -/** - * Copyright (C) 2013 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; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.os.AsyncTask; -import android.os.Bundle; -import androidx.preference.PreferenceManager; -import android.view.View; -import android.widget.ProgressBar; - -import org.thoughtcrime.securesms.attachments.DatabaseAttachment; -import org.thoughtcrime.securesms.color.MaterialColor; -import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy; -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; -import org.thoughtcrime.securesms.crypto.MasterSecret; -import org.thoughtcrime.securesms.database.AttachmentDatabase; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.database.MmsDatabase.Reader; -import org.thoughtcrime.securesms.database.PushDatabase; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; -import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob; -import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; -import org.thoughtcrime.securesms.jobs.PushDecryptJob; -import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; -import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.mms.GlideApp; -import org.thoughtcrime.securesms.notifications.MessageNotifier; -import org.thoughtcrime.securesms.service.KeyCachingService; -import org.thoughtcrime.securesms.util.FileUtils; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Util; -import org.thoughtcrime.securesms.util.VersionTracker; - -import java.io.File; -import java.io.IOException; -import java.util.List; -import java.util.SortedSet; -import java.util.TreeSet; - -public class DatabaseUpgradeActivity extends BaseActivity { - private static final String TAG = DatabaseUpgradeActivity.class.getSimpleName(); - - public static final int NO_MORE_KEY_EXCHANGE_PREFIX_VERSION = 46; - public static final int MMS_BODY_VERSION = 46; - public static final int TOFU_IDENTITIES_VERSION = 50; - public static final int CURVE25519_VERSION = 63; - public static final int ASYMMETRIC_MASTER_SECRET_FIX_VERSION = 73; - public static final int NO_V1_VERSION = 83; - public static final int SIGNED_PREKEY_VERSION = 83; - public static final int NO_DECRYPT_QUEUE_VERSION = 113; - public static final int PUSH_DECRYPT_SERIAL_ID_VERSION = 131; - public static final int MIGRATE_SESSION_PLAINTEXT = 136; - public static final int CONTACTS_ACCOUNT_VERSION = 136; - public static final int MEDIA_DOWNLOAD_CONTROLS_VERSION = 151; - public static final int REDPHONE_SUPPORT_VERSION = 157; - public static final int NO_MORE_CANONICAL_DB_VERSION = 276; - public static final int PROFILES = 289; - public static final int SCREENSHOTS = 300; - public static final int PERSISTENT_BLOBS = 317; - public static final int INTERNALIZE_CONTACTS = 317; - public static final int SQLCIPHER = 334; - public static final int SQLCIPHER_COMPLETE = 352; - public static final int REMOVE_JOURNAL = 353; - public static final int REMOVE_CACHE = 354; - public static final int FULL_TEXT_SEARCH = 358; - public static final int BAD_IMPORT_CLEANUP = 373; - public static final int IMAGE_CACHE_CLEANUP = 406; - public static final int WORKMANAGER_MIGRATION = 408; - public static final int COLOR_MIGRATION = 412; - public static final int UNIDENTIFIED_DELIVERY = 422; - public static final int SIGNALING_KEY_DEPRECATION = 447; - public static final int CONVERSATION_SEARCH = 455; - - private static final SortedSet UPGRADE_VERSIONS = new TreeSet() {{ - add(NO_MORE_KEY_EXCHANGE_PREFIX_VERSION); - add(TOFU_IDENTITIES_VERSION); - add(CURVE25519_VERSION); - add(ASYMMETRIC_MASTER_SECRET_FIX_VERSION); - add(NO_V1_VERSION); - add(SIGNED_PREKEY_VERSION); - add(NO_DECRYPT_QUEUE_VERSION); - add(PUSH_DECRYPT_SERIAL_ID_VERSION); - add(MIGRATE_SESSION_PLAINTEXT); - add(MEDIA_DOWNLOAD_CONTROLS_VERSION); - add(REDPHONE_SUPPORT_VERSION); - add(NO_MORE_CANONICAL_DB_VERSION); - add(SCREENSHOTS); - add(INTERNALIZE_CONTACTS); - add(PERSISTENT_BLOBS); - add(SQLCIPHER); - add(SQLCIPHER_COMPLETE); - add(REMOVE_CACHE); - add(FULL_TEXT_SEARCH); - add(BAD_IMPORT_CLEANUP); - add(IMAGE_CACHE_CLEANUP); - add(WORKMANAGER_MIGRATION); - add(COLOR_MIGRATION); - add(UNIDENTIFIED_DELIVERY); - add(SIGNALING_KEY_DEPRECATION); - add(CONVERSATION_SEARCH); - }}; - - private MasterSecret masterSecret; - - @Override - public void onCreate(Bundle bundle) { - super.onCreate(bundle); - this.masterSecret = KeyCachingService.getMasterSecret(this); - - if (needsUpgradeTask()) { - Log.i("DatabaseUpgradeActivity", "Upgrading..."); - setContentView(R.layout.database_upgrade_activity); - - ProgressBar indeterminateProgress = findViewById(R.id.indeterminate_progress); - ProgressBar determinateProgress = findViewById(R.id.determinate_progress); - - new DatabaseUpgradeTask(indeterminateProgress, determinateProgress) - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, VersionTracker.getLastSeenVersion(this)); - } else { - VersionTracker.updateLastSeenVersion(this); - updateNotifications(this); - startActivity((Intent)getIntent().getParcelableExtra("next_intent")); - finish(); - } - } - - private boolean needsUpgradeTask() { - int currentVersionCode = Util.getCanonicalVersionCode(); - int lastSeenVersion = VersionTracker.getLastSeenVersion(this); - - Log.i("DatabaseUpgradeActivity", "LastSeenVersion: " + lastSeenVersion); - - if (lastSeenVersion >= currentVersionCode) - return false; - - for (int version : UPGRADE_VERSIONS) { - Log.i("DatabaseUpgradeActivity", "Comparing: " + version); - if (lastSeenVersion < version) - return true; - } - - return false; - } - - public static boolean isUpdate(Context context) { - int currentVersionCode = Util.getCanonicalVersionCode(); - int previousVersionCode = VersionTracker.getLastSeenVersion(context); - - return previousVersionCode < currentVersionCode; - } - - @SuppressLint("StaticFieldLeak") - private void updateNotifications(final Context context) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - MessageNotifier.updateNotification(context); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - public interface DatabaseUpgradeListener { - public void setProgress(int progress, int total); - } - - @SuppressLint("StaticFieldLeak") - private class DatabaseUpgradeTask extends AsyncTask - implements DatabaseUpgradeListener - { - - private final ProgressBar indeterminateProgress; - private final ProgressBar determinateProgress; - - DatabaseUpgradeTask(ProgressBar indeterminateProgress, ProgressBar determinateProgress) { - this.indeterminateProgress = indeterminateProgress; - this.determinateProgress = determinateProgress; - } - - @Override - protected Void doInBackground(Integer... params) { - Context context = DatabaseUpgradeActivity.this.getApplicationContext(); - - Log.i("DatabaseUpgradeActivity", "Running background upgrade.."); - DatabaseFactory.getInstance(DatabaseUpgradeActivity.this) - .onApplicationLevelUpgrade(context, masterSecret, params[0], this); - - if (params[0] < CURVE25519_VERSION) { - IdentityKeyUtil.migrateIdentityKeys(context, masterSecret); - } - - if (params[0] < NO_V1_VERSION) { - File v1sessions = new File(context.getFilesDir(), "sessions"); - - if (v1sessions.exists() && v1sessions.isDirectory()) { - File[] contents = v1sessions.listFiles(); - - if (contents != null) { - for (File session : contents) { - session.delete(); - } - } - - v1sessions.delete(); - } - } - - if (params[0] < SIGNED_PREKEY_VERSION) { - ApplicationContext.getInstance(getApplicationContext()) - .getJobManager() - .add(new CreateSignedPreKeyJob(context)); - } - - if (params[0] < NO_DECRYPT_QUEUE_VERSION) { - scheduleMessagesInPushDatabase(context); - } - - if (params[0] < PUSH_DECRYPT_SERIAL_ID_VERSION) { - scheduleMessagesInPushDatabase(context); - } - - if (params[0] < MIGRATE_SESSION_PLAINTEXT) { -// new TextSecureSessionStore(context, masterSecret).migrateSessions(); -// new TextSecurePreKeyStore(context, masterSecret).migrateRecords(); - - IdentityKeyUtil.migrateIdentityKeys(context, masterSecret); - scheduleMessagesInPushDatabase(context);; - } - - if (params[0] < CONTACTS_ACCOUNT_VERSION) { - ApplicationContext.getInstance(getApplicationContext()) - .getJobManager() - .add(new DirectoryRefreshJob(false)); - } - - if (params[0] < MEDIA_DOWNLOAD_CONTROLS_VERSION) { - schedulePendingIncomingParts(context); - } - - if (params[0] < REDPHONE_SUPPORT_VERSION) { - ApplicationContext.getInstance(getApplicationContext()) - .getJobManager() - .add(new RefreshAttributesJob()); - ApplicationContext.getInstance(getApplicationContext()) - .getJobManager() - .add(new DirectoryRefreshJob(false)); - } - - if (params[0] < PROFILES) { - ApplicationContext.getInstance(getApplicationContext()) - .getJobManager() - .add(new DirectoryRefreshJob(false)); - } - - if (params[0] < SCREENSHOTS) { - boolean screenSecurity = PreferenceManager.getDefaultSharedPreferences(context).getBoolean(TextSecurePreferences.SCREEN_SECURITY_PREF, true); - TextSecurePreferences.setScreenSecurityEnabled(getApplicationContext(), screenSecurity); - } - - if (params[0] < PERSISTENT_BLOBS) { - File externalDir = context.getExternalFilesDir(null); - - if (externalDir != null && externalDir.isDirectory() && externalDir.exists()) { - for (File blob : externalDir.listFiles()) { - if (blob.exists() && blob.isFile()) blob.delete(); - } - } - } - - if (params[0] < INTERNALIZE_CONTACTS) { - if (TextSecurePreferences.isPushRegistered(getApplicationContext())) { - TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(getApplicationContext(), true); - } - } - - if (params[0] < SQLCIPHER) { - scheduleMessagesInPushDatabase(context); - } - - if (params[0] < SQLCIPHER_COMPLETE) { - File file = context.getDatabasePath("messages.db"); - if (file != null && file.exists()) file.delete(); - } - - if (params[0] < REMOVE_JOURNAL) { - File file = context.getDatabasePath("messages.db-journal"); - if (file != null && file.exists()) file.delete(); - } - - if (params[0] < REMOVE_CACHE) { - try { - FileUtils.deleteDirectoryContents(context.getCacheDir()); - } catch (IOException e) { - Log.w(TAG, e); - } - } - - if (params[0] < IMAGE_CACHE_CLEANUP) { - try { - FileUtils.deleteDirectoryContents(context.getExternalCacheDir()); - GlideApp.get(context).clearDiskCache(); - } catch (IOException e) { - Log.w(TAG, e); - } - } - - // 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(); - DatabaseFactory.getRecipientDatabase(context).updateSystemContactColors((name, color) -> { - if (color != null) { - try { - return MaterialColor.fromSerialized(color); - } catch (MaterialColor.UnknownColorException e) { - Log.w(TAG, "Encountered an unknown color during legacy color migration.", e); - return ContactColorsLegacy.generateFor(name); - } - } - return ContactColorsLegacy.generateFor(name); - }); - Log.i(TAG, "Color migration took " + (System.currentTimeMillis() - startTime) + " ms"); - } - - if (params[0] < UNIDENTIFIED_DELIVERY) { - if (TextSecurePreferences.isMultiDevice(context)) { - Log.i(TAG, "MultiDevice: Disabling UD (will be re-enabled if possible after pending refresh)."); - TextSecurePreferences.setIsUnidentifiedDeliveryEnabled(context, false); - } - - Log.i(TAG, "Scheduling UD attributes refresh."); - ApplicationContext.getInstance(context) - .getJobManager() - .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()); - } - - return null; - } - - private void schedulePendingIncomingParts(Context context) { - final AttachmentDatabase attachmentDb = DatabaseFactory.getAttachmentDatabase(context); - final MmsDatabase mmsDb = DatabaseFactory.getMmsDatabase(context); - final List pendingAttachments = DatabaseFactory.getAttachmentDatabase(context).getPendingAttachments(); - - Log.i(TAG, pendingAttachments.size() + " pending parts."); - for (DatabaseAttachment attachment : pendingAttachments) { - final Reader reader = mmsDb.readerFor(mmsDb.getMessage(attachment.getMmsId())); - final MessageRecord record = reader.getNext(); - - if (attachment.hasData()) { - Log.i(TAG, "corrected a pending media part " + attachment.getAttachmentId() + "that already had data."); - attachmentDb.setTransferState(attachment.getMmsId(), attachment.getAttachmentId(), AttachmentDatabase.TRANSFER_PROGRESS_DONE); - } else if (record != null && !record.isOutgoing() && record.isPush()) { - Log.i(TAG, "queuing new attachment download job for incoming push part " + attachment.getAttachmentId() + "."); - ApplicationContext.getInstance(context) - .getJobManager() - .add(new AttachmentDownloadJob(attachment.getMmsId(), attachment.getAttachmentId(), false)); - } - reader.close(); - } - } - - private void scheduleMessagesInPushDatabase(Context context) { - PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(context); - Cursor pushReader = null; - - try { - pushReader = pushDatabase.getPending(); - - while (pushReader != null && pushReader.moveToNext()) { - ApplicationContext.getInstance(getApplicationContext()) - .getJobManager() - .add(new PushDecryptJob(getApplicationContext(), - pushReader.getLong(pushReader.getColumnIndexOrThrow(PushDatabase.ID)))); - } - } finally { - if (pushReader != null) - pushReader.close(); - } - } - - @Override - protected void onProgressUpdate(Double... update) { - indeterminateProgress.setVisibility(View.GONE); - determinateProgress.setVisibility(View.VISIBLE); - - double scaler = update[0]; - determinateProgress.setProgress((int)Math.floor(determinateProgress.getMax() * scaler)); - } - - @Override - protected void onPostExecute(Void result) { - VersionTracker.updateLastSeenVersion(DatabaseUpgradeActivity.this); - updateNotifications(DatabaseUpgradeActivity.this); - - startActivity((Intent)getIntent().getParcelableExtra("next_intent")); - finish(); - } - - @Override - public void setProgress(int progress, int total) { - publishProgress(((double)progress / (double)total)); - } - } - -} diff --git a/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java b/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java index 691b4de983..ce2e48871e 100644 --- a/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java +++ b/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java @@ -13,6 +13,8 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; +import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity; +import org.thoughtcrime.securesms.migrations.ApplicationMigrations; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.registration.WelcomeActivity; import org.thoughtcrime.securesms.service.KeyCachingService; @@ -38,7 +40,6 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA @Override protected final void onCreate(Bundle savedInstanceState) { - Log.i(TAG, "onCreate(" + savedInstanceState + ")"); this.networkAccess = new SignalServiceNetworkAccess(this); onPreCreate(); @@ -145,7 +146,7 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA return STATE_CREATE_PASSPHRASE; } else if (locked) { return STATE_PROMPT_PASSPHRASE; - } else if (DatabaseUpgradeActivity.isUpdate(this)) { + } else if (ApplicationMigrations.isUpdate(this)) { return STATE_UPGRADE_DATABASE; } else if (!TextSecurePreferences.hasSeenWelcomeScreen(this)) { return STATE_WELCOME_SCREEN; @@ -167,7 +168,7 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA } private Intent getUpgradeDatabaseIntent() { - return getRoutedIntent(DatabaseUpgradeActivity.class, + return getRoutedIntent(ApplicationMigrationActivity.class, TextSecurePreferences.hasPromptedPushRegistration(this) ? getConversationListIntent() : getPushRegistrationIntent()); diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index 23b36dc63f..c459e1dfea 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -21,7 +21,6 @@ import androidx.annotation.NonNull; import net.sqlcipher.database.SQLiteDatabase; -import org.thoughtcrime.securesms.DatabaseUpgradeActivity; import org.thoughtcrime.securesms.contacts.ContactsDatabase; import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; @@ -31,6 +30,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.thoughtcrime.securesms.migrations.LegacyMigrationJob; import org.thoughtcrime.securesms.util.TextSecurePreferences; public class DatabaseFactory { @@ -184,18 +184,18 @@ public class DatabaseFactory { } public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret, - int fromVersion, DatabaseUpgradeActivity.DatabaseUpgradeListener listener) + int fromVersion, LegacyMigrationJob.DatabaseUpgradeListener listener) { databaseHelper.getWritableDatabase(); ClassicOpenHelper legacyOpenHelper = null; - if (fromVersion < DatabaseUpgradeActivity.ASYMMETRIC_MASTER_SECRET_FIX_VERSION) { + if (fromVersion < LegacyMigrationJob.ASYMMETRIC_MASTER_SECRET_FIX_VERSION) { legacyOpenHelper = new ClassicOpenHelper(context); legacyOpenHelper.onApplicationLevelUpgrade(context, masterSecret, fromVersion, listener); } - if (fromVersion < DatabaseUpgradeActivity.SQLCIPHER && TextSecurePreferences.getNeedsSqlCipherMigration(context)) { + if (fromVersion < LegacyMigrationJob.SQLCIPHER && TextSecurePreferences.getNeedsSqlCipherMigration(context)) { if (legacyOpenHelper == null) { legacyOpenHelper = new ClassicOpenHelper(context); } @@ -206,4 +206,8 @@ public class DatabaseFactory { listener); } } + + public void triggerDatabaseAccess() { + databaseHelper.getWritableDatabase(); + } } diff --git a/src/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java index 0c47493090..c1a875d88b 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java @@ -20,7 +20,6 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber; import com.google.i18n.phonenumbers.ShortNumberInfo; -import org.thoughtcrime.securesms.DatabaseUpgradeActivity; import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.MasterCipher; @@ -37,6 +36,7 @@ import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.migrations.LegacyMigrationJob; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.util.Base64; @@ -146,12 +146,12 @@ public class ClassicOpenHelper extends SQLiteOpenHelper { } public void onApplicationLevelUpgrade(Context context, MasterSecret masterSecret, int fromVersion, - DatabaseUpgradeActivity.DatabaseUpgradeListener listener) + LegacyMigrationJob.DatabaseUpgradeListener listener) { SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); - if (fromVersion < DatabaseUpgradeActivity.NO_MORE_KEY_EXCHANGE_PREFIX_VERSION) { + if (fromVersion < LegacyMigrationJob.NO_MORE_KEY_EXCHANGE_PREFIX_VERSION) { String KEY_EXCHANGE = "?TextSecureKeyExchange"; String PROCESSED_KEY_EXCHANGE = "?TextSecureKeyExchangd"; String STALE_KEY_EXCHANGE = "?TextSecureKeyExchangs"; @@ -293,7 +293,7 @@ public class ClassicOpenHelper extends SQLiteOpenHelper { threadCursor.close(); } - if (fromVersion < DatabaseUpgradeActivity.MMS_BODY_VERSION) { + if (fromVersion < LegacyMigrationJob.MMS_BODY_VERSION) { Log.i("DatabaseFactory", "Update MMS bodies..."); MasterCipher masterCipher = new MasterCipher(masterSecret); Cursor mmsCursor = db.query("mms", new String[] {"_id"}, @@ -357,7 +357,7 @@ public class ClassicOpenHelper extends SQLiteOpenHelper { } } - if (fromVersion < DatabaseUpgradeActivity.TOFU_IDENTITIES_VERSION) { + if (fromVersion < LegacyMigrationJob.TOFU_IDENTITIES_VERSION) { File sessionDirectory = new File(context.getFilesDir() + File.separator + "sessions"); if (sessionDirectory.exists() && sessionDirectory.isDirectory()) { @@ -393,7 +393,7 @@ public class ClassicOpenHelper extends SQLiteOpenHelper { } } - if (fromVersion < DatabaseUpgradeActivity.ASYMMETRIC_MASTER_SECRET_FIX_VERSION) { + if (fromVersion < LegacyMigrationJob.ASYMMETRIC_MASTER_SECRET_FIX_VERSION) { if (!MasterSecretUtil.hasAsymmericMasterSecret(context)) { MasterSecretUtil.generateAsymmetricMasterSecret(context, masterSecret); diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherMigrationHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherMigrationHelper.java index 02682b46b6..ea6e2b73f1 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherMigrationHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherMigrationHelper.java @@ -12,13 +12,13 @@ import android.util.Pair; import com.annimon.stream.function.BiFunction; -import org.thoughtcrime.securesms.DatabaseUpgradeActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; +import org.thoughtcrime.securesms.migrations.LegacyMigrationJob; import org.thoughtcrime.securesms.service.GenericForegroundService; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -58,7 +58,7 @@ public class SQLCipherMigrationHelper { @NonNull MasterSecret masterSecret, @NonNull android.database.sqlite.SQLiteDatabase legacyDb, @NonNull net.sqlcipher.database.SQLiteDatabase modernDb, - @Nullable DatabaseUpgradeActivity.DatabaseUpgradeListener listener) + @Nullable LegacyMigrationJob.DatabaseUpgradeListener listener) { MasterCipher legacyCipher = new MasterCipher(masterSecret); AsymmetricMasterCipher legacyAsymmetricCipher = new AsymmetricMasterCipher(MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret)); diff --git a/src/org/thoughtcrime/securesms/jobmanager/Job.java b/src/org/thoughtcrime/securesms/jobmanager/Job.java index 790b83f708..45665440a2 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/Job.java +++ b/src/org/thoughtcrime/securesms/jobmanager/Job.java @@ -199,8 +199,9 @@ public abstract class Job { public static final class Parameters { - public static final int IMMORTAL = -1; - public static final int UNLIMITED = -1; + public static final String MIGRATION_QUEUE_KEY = "MIGRATION"; + public static final int IMMORTAL = -1; + public static final int UNLIMITED = -1; private final long createTime; private final long lifespan; @@ -255,16 +256,41 @@ public abstract class Job { return constraintKeys; } + public Builder toBuilder() { + return new Builder(createTime, maxBackoff, lifespan, maxAttempts, maxInstances, queue, constraintKeys); + } + 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<>(); + private long createTime; + private long maxBackoff; + private long lifespan; + private int maxAttempts; + private int maxInstances; + private String queue; + private List constraintKeys; + + public Builder() { + this(System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(30), IMMORTAL, 1, UNLIMITED, null, new LinkedList<>()); + } + + private Builder(long createTime, + long maxBackoff, + long lifespan, + int maxAttempts, + int maxInstances, + @Nullable String queue, + @NonNull List constraintKeys) + { + this.createTime = createTime; + this.maxBackoff = maxBackoff; + this.lifespan = lifespan; + this.maxAttempts = maxAttempts; + this.maxInstances = maxInstances; + this.queue = queue; + this.constraintKeys = constraintKeys; + } /** Should only be invoked by {@link JobController} */ Builder setCreateTime(long createTime) { diff --git a/src/org/thoughtcrime/securesms/jobmanager/JobManager.java b/src/org/thoughtcrime/securesms/jobmanager/JobManager.java index 327372552d..c05102e8c4 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/JobManager.java +++ b/src/org/thoughtcrime/securesms/jobmanager/JobManager.java @@ -31,15 +31,17 @@ public class JobManager implements ConstraintObserver.Notifier { private static final String TAG = JobManager.class.getSimpleName(); + private final Application application; + private final Configuration configuration; private final ExecutorService executor; private final JobController jobController; - private final JobRunner[] jobRunners; private final Set emptyQueueListeners = new CopyOnWriteArraySet<>(); public JobManager(@NonNull Application application, @NonNull Configuration configuration) { + this.application = application; + this.configuration = configuration; this.executor = configuration.getExecutorFactory().newSingleThreadExecutor("signal-JobManager"); - this.jobRunners = new JobRunner[configuration.getJobThreadCount()]; this.jobController = new JobController(application, configuration.getJobStorage(), configuration.getJobInstantiator(), @@ -58,11 +60,6 @@ public class JobManager implements ConstraintObserver.Notifier { jobController.init(); - for (int i = 0; i < jobRunners.length; i++) { - jobRunners[i] = new JobRunner(application, i + 1, jobController); - jobRunners[i].start(); - } - for (ConstraintObserver constraintObserver : configuration.getConstraintObservers()) { constraintObserver.register(this); } @@ -70,7 +67,17 @@ public class JobManager implements ConstraintObserver.Notifier { if (Build.VERSION.SDK_INT < 26) { application.startService(new Intent(application, KeepAliveService.class)); } + }); + } + /** + * Begins the execution of jobs. + */ + public void beginJobLoop() { + executor.execute(() -> { + for (int i = 0; i < configuration.getJobThreadCount(); i++) { + new JobRunner(application, i + 1, jobController).start(); + } wakeUp(); }); } @@ -112,7 +119,7 @@ public class JobManager implements ConstraintObserver.Notifier { } /** - * Adds a listener to that will be notified when the job queue has been drained. + * Adds a listener that will be notified when the job queue has been drained. */ void addOnEmptyQueueListener(@NonNull EmptyQueueListener listener) { executor.execute(() -> { diff --git a/src/org/thoughtcrime/securesms/jobmanager/persistence/FullSpec.java b/src/org/thoughtcrime/securesms/jobmanager/persistence/FullSpec.java index f93c0e64bd..563c36a80b 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/persistence/FullSpec.java +++ b/src/org/thoughtcrime/securesms/jobmanager/persistence/FullSpec.java @@ -15,7 +15,7 @@ public final class FullSpec { @NonNull List constraintSpecs, @NonNull List dependencySpecs) { - this.jobSpec = jobSpec; + this.jobSpec = jobSpec; this.constraintSpecs = constraintSpecs; this.dependencySpecs = dependencySpecs; } diff --git a/src/org/thoughtcrime/securesms/jobs/FastJobStorage.java b/src/org/thoughtcrime/securesms/jobs/FastJobStorage.java index 38cf051455..2997198382 100644 --- a/src/org/thoughtcrime/securesms/jobs/FastJobStorage.java +++ b/src/org/thoughtcrime/securesms/jobs/FastJobStorage.java @@ -6,12 +6,14 @@ import androidx.annotation.Nullable; import com.annimon.stream.Stream; import org.thoughtcrime.securesms.database.JobDatabase; +import org.thoughtcrime.securesms.jobmanager.Job; 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 org.whispersystems.libsignal.util.guava.Optional; import java.util.ArrayList; import java.util.Collections; @@ -88,13 +90,29 @@ public class FastJobStorage implements JobStorage { @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(); + Optional migrationJob = getMigrationJob(); + + if (migrationJob.isPresent() && !migrationJob.get().isRunning()) { + return Collections.singletonList(migrationJob.get()); + } else if (migrationJob.isPresent()) { + return Collections.emptyList(); + } else { + 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 Optional getMigrationJob() { + return Optional.fromNullable(Stream.of(jobs) + .filter(j -> Job.Parameters.MIGRATION_QUEUE_KEY.equals(j.getQueueKey())) + .filter(this::firstInQueue) + .findFirst() + .orElse(null)); } private boolean firstInQueue(@NonNull JobSpec job) { diff --git a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 58ffd28aba..deb775c0c8 100644 --- a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -13,6 +13,9 @@ 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 org.thoughtcrime.securesms.migrations.DatabaseMigrationJob; +import org.thoughtcrime.securesms.migrations.LegacyMigrationJob; +import org.thoughtcrime.securesms.migrations.MigrationCompleteJob; import java.util.Arrays; import java.util.HashMap; @@ -72,6 +75,11 @@ public final class JobManagerFactories { put(TypingSendJob.KEY, new TypingSendJob.Factory()); put(UpdateApkJob.KEY, new UpdateApkJob.Factory()); + // Migrations + put(DatabaseMigrationJob.KEY, new DatabaseMigrationJob.Factory()); + put(LegacyMigrationJob.KEY, new LegacyMigrationJob.Factory()); + put(MigrationCompleteJob.KEY, new MigrationCompleteJob.Factory()); + // Dead jobs put("PushContentReceiveJob", new FailingJob.Factory()); }}; diff --git a/src/org/thoughtcrime/securesms/migrations/ApplicationMigrationActivity.java b/src/org/thoughtcrime/securesms/migrations/ApplicationMigrationActivity.java new file mode 100644 index 0000000000..4273d06653 --- /dev/null +++ b/src/org/thoughtcrime/securesms/migrations/ApplicationMigrationActivity.java @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2019 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.migrations; + +import android.os.Bundle; + +import org.thoughtcrime.securesms.BaseActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.logging.Log; + +/** + * An activity that can be shown to block access to the rest of the app when a long-running or + * otherwise blocking application-level migration is happening. + */ +public class ApplicationMigrationActivity extends BaseActivity { + + private static final String TAG = Log.tag(ApplicationMigrationActivity.class); + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + + ApplicationMigrations.isUiBlockingMigrationRunning().observe(this, running -> { + if (running == null) { + return; + } + + if (running) { + Log.i(TAG, "UI-blocking migration is in progress. Showing spinner."); + setContentView(R.layout.application_migration_activity); + } else { + Log.i(TAG, "UI-blocking migration is no-longer in progress. Finishing."); + startActivity(getIntent().getParcelableExtra("next_intent")); + finish(); + } + }); + } +} diff --git a/src/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/src/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java new file mode 100644 index 0000000000..3a99fcd13d --- /dev/null +++ b/src/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -0,0 +1,133 @@ +package org.thoughtcrime.securesms.migrations; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.annimon.stream.Stream; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; +import org.thoughtcrime.securesms.jobmanager.JobManager; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.VersionTracker; + +import java.util.LinkedList; +import java.util.List; + +/** + * Manages application-level migrations. + * + * Migrations can be slotted to occur based on changes in the canonical version code + * (see {@link Util#getCanonicalVersionCode()}). + * + * Migrations are performed via {@link MigrationJob}s. These jobs are durable and are run before any + * other job, allowing you to schedule safe migrations. Furthermore, you may specify that a + * migration is UI-blocking, at which point we will show a spinner via + * {@link ApplicationMigrationActivity} if the user opens the app while the migration is in + * progress. + */ +public class ApplicationMigrations { + + private static final String TAG = Log.tag(ApplicationMigrations.class); + + private static final MutableLiveData UI_BLOCKING_MIGRATION_RUNNING = new MutableLiveData<>(); + + private static final class Version { + static final int LEGACY = 455; + } + + /** + * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call + * to {@link JobManager#beginJobLoop()}. Otherwise, other non-migration jobs may have started + * executing before we add the migration jobs. + */ + public static void onApplicationCreate(@NonNull Context context, @NonNull JobManager jobManager) { + if (!isUpdate(context)) { + Log.d(TAG, "Not an update. Skipping."); + return; + } + + final int currentVersion = Util.getCanonicalVersionCode(); + final int lastSeenVersion = VersionTracker.getLastSeenVersion(context); + + Log.d(TAG, "currentVersion: " + currentVersion + " lastSeenVersion: " + lastSeenVersion); + + List migrationJobs = getMigrationJobs(context, lastSeenVersion); + + if (migrationJobs.size() > 0) { + Log.i(TAG, "About to enqueue " + migrationJobs.size() + " migration(s)."); + + boolean uiBlocking = Stream.of(migrationJobs).reduce(false, (existing, job) -> existing || job.isUiBlocking()); + UI_BLOCKING_MIGRATION_RUNNING.postValue(uiBlocking); + + if (uiBlocking) { + Log.i(TAG, "Migration set is UI-blocking."); + } else { + Log.i(TAG, "Migration set is non-UI-blocking."); + } + + for (MigrationJob job : migrationJobs) { + jobManager.add(job); + } + + jobManager.add(new MigrationCompleteJob(currentVersion)); + + final long startTime = System.currentTimeMillis(); + + EventBus.getDefault().register(new Object() { + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onMigrationComplete(MigrationCompleteEvent event) { + Log.i(TAG, "Received MigrationCompleteEvent for version " + event.getVersion() + "."); + + if (event.getVersion() == currentVersion) { + Log.i(TAG, "Migration complete. Took " + (System.currentTimeMillis() - startTime) + " ms."); + EventBus.getDefault().unregister(this); + + VersionTracker.updateLastSeenVersion(context); + UI_BLOCKING_MIGRATION_RUNNING.postValue(false); + } else { + Log.i(TAG, "Version doesn't match. Looking for " + currentVersion + ", but received " + event.getVersion() + "."); + } + } + }); + } else { + Log.d(TAG, "No migrations."); + VersionTracker.updateLastSeenVersion(context); + UI_BLOCKING_MIGRATION_RUNNING.postValue(false); + } + } + + /** + * @return A {@link LiveData} object that will update with whether or not a UI blocking migration + * is in progress. + */ + public static LiveData isUiBlockingMigrationRunning() { + return UI_BLOCKING_MIGRATION_RUNNING; + } + + /** + * @return Whether or not we're in the middle of an update, as determined by the last seen and + * current version. + */ + public static boolean isUpdate(Context context) { + int currentVersionCode = Util.getCanonicalVersionCode(); + int previousVersionCode = VersionTracker.getLastSeenVersion(context); + + return previousVersionCode < currentVersionCode; + } + + private static List getMigrationJobs(@NonNull Context context, int lastSeenVersion) { + List jobs = new LinkedList<>(); + + if (lastSeenVersion < Version.LEGACY) { + jobs.add(new LegacyMigrationJob()); + } + + return jobs; + } +} diff --git a/src/org/thoughtcrime/securesms/migrations/DatabaseMigrationJob.java b/src/org/thoughtcrime/securesms/migrations/DatabaseMigrationJob.java new file mode 100644 index 0000000000..443c42e4d4 --- /dev/null +++ b/src/org/thoughtcrime/securesms/migrations/DatabaseMigrationJob.java @@ -0,0 +1,51 @@ +package org.thoughtcrime.securesms.migrations; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; + +/** + * Triggers a database access, forcing the database to upgrade if it hasn't already. Should be used + * when you expect a database migration to take a particularly long time. + */ +public class DatabaseMigrationJob extends MigrationJob { + + public static final String KEY = "DatabaseMigrationJob"; + + DatabaseMigrationJob() { + this(new Parameters.Builder().build()); + } + + private DatabaseMigrationJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + public boolean isUiBlocking() { + return true; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void performMigration() { + DatabaseFactory.getInstance(context).triggerDatabaseAccess(); + } + + @Override + boolean shouldRetry(@NonNull Exception e) { + return false; + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull DatabaseMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new DatabaseMigrationJob(parameters); + } + } +} diff --git a/src/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java b/src/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java new file mode 100644 index 0000000000..952c412923 --- /dev/null +++ b/src/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java @@ -0,0 +1,341 @@ +package org.thoughtcrime.securesms.migrations; + +import android.content.Context; +import android.database.Cursor; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; + +import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.attachments.DatabaseAttachment; +import org.thoughtcrime.securesms.color.MaterialColor; +import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy; +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.MmsDatabase.Reader; +import org.thoughtcrime.securesms.database.PushDatabase; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; +import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob; +import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; +import org.thoughtcrime.securesms.jobs.PushDecryptJob; +import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.mms.GlideApp; +import org.thoughtcrime.securesms.service.KeyCachingService; +import org.thoughtcrime.securesms.transport.RetryLaterException; +import org.thoughtcrime.securesms.util.FileUtils; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.VersionTracker; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * Represents all of the migrations that used to take place in {@link ApplicationMigrationActivity} + * (previously known as DatabaseUpgradeActivity). This job should *never* have new versions or + * migrations added to it. Instead, create a new {@link MigrationJob} and place it in + * {@link ApplicationMigrations}. + */ +public class LegacyMigrationJob extends MigrationJob { + + public static final String KEY = "LegacyMigrationJob"; + + private static final String TAG = Log.tag(LegacyMigrationJob.class); + + public static final int NO_MORE_KEY_EXCHANGE_PREFIX_VERSION = 46; + public static final int MMS_BODY_VERSION = 46; + public static final int TOFU_IDENTITIES_VERSION = 50; + private static final int CURVE25519_VERSION = 63; + public static final int ASYMMETRIC_MASTER_SECRET_FIX_VERSION = 73; + private static final int NO_V1_VERSION = 83; + private static final int SIGNED_PREKEY_VERSION = 83; + private static final int NO_DECRYPT_QUEUE_VERSION = 113; + private static final int PUSH_DECRYPT_SERIAL_ID_VERSION = 131; + private static final int MIGRATE_SESSION_PLAINTEXT = 136; + private static final int CONTACTS_ACCOUNT_VERSION = 136; + private static final int MEDIA_DOWNLOAD_CONTROLS_VERSION = 151; + private static final int REDPHONE_SUPPORT_VERSION = 157; + private static final int NO_MORE_CANONICAL_DB_VERSION = 276; + private static final int PROFILES = 289; + private static final int SCREENSHOTS = 300; + private static final int PERSISTENT_BLOBS = 317; + private static final int INTERNALIZE_CONTACTS = 317; + public static final int SQLCIPHER = 334; + private static final int SQLCIPHER_COMPLETE = 352; + private static final int REMOVE_JOURNAL = 353; + private static final int REMOVE_CACHE = 354; + private static final int FULL_TEXT_SEARCH = 358; + private static final int BAD_IMPORT_CLEANUP = 373; + private static final int IMAGE_CACHE_CLEANUP = 406; + private static final int WORKMANAGER_MIGRATION = 408; + private static final int COLOR_MIGRATION = 412; + private static final int UNIDENTIFIED_DELIVERY = 422; + private static final int SIGNALING_KEY_DEPRECATION = 447; + private static final int CONVERSATION_SEARCH = 455; + + + public LegacyMigrationJob() { + this(new Parameters.Builder().build()); + } + + private LegacyMigrationJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + public boolean isUiBlocking() { + return true; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + void performMigration() throws RetryLaterException { + Log.i("DatabaseUpgradeActivity", "Running background upgrade.."); + int lastSeenVersion = VersionTracker.getLastSeenVersion(context); + MasterSecret masterSecret = KeyCachingService.getMasterSecret(context); + + if (lastSeenVersion < SQLCIPHER && masterSecret != null) { + DatabaseFactory.getInstance(context).onApplicationLevelUpgrade(context, masterSecret, lastSeenVersion, (progress, total) -> { + Log.i(TAG, "onApplicationLevelUpgrade: " + progress + "/" + total); + }); + } else if (lastSeenVersion < SQLCIPHER) { + throw new RetryLaterException(); + } + + if (lastSeenVersion < CURVE25519_VERSION) { + IdentityKeyUtil.migrateIdentityKeys(context, masterSecret); + } + + if (lastSeenVersion < NO_V1_VERSION) { + File v1sessions = new File(context.getFilesDir(), "sessions"); + + if (v1sessions.exists() && v1sessions.isDirectory()) { + File[] contents = v1sessions.listFiles(); + + if (contents != null) { + for (File session : contents) { + session.delete(); + } + } + + v1sessions.delete(); + } + } + + if (lastSeenVersion < SIGNED_PREKEY_VERSION) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new CreateSignedPreKeyJob(context)); + } + + if (lastSeenVersion < NO_DECRYPT_QUEUE_VERSION) { + scheduleMessagesInPushDatabase(context); + } + + if (lastSeenVersion < PUSH_DECRYPT_SERIAL_ID_VERSION) { + scheduleMessagesInPushDatabase(context); + } + + if (lastSeenVersion < MIGRATE_SESSION_PLAINTEXT) { +// new TextSecureSessionStore(context, masterSecret).migrateSessions(); +// new TextSecurePreKeyStore(context, masterSecret).migrateRecords(); + + IdentityKeyUtil.migrateIdentityKeys(context, masterSecret); + scheduleMessagesInPushDatabase(context);; + } + + if (lastSeenVersion < CONTACTS_ACCOUNT_VERSION) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new DirectoryRefreshJob(false)); + } + + if (lastSeenVersion < MEDIA_DOWNLOAD_CONTROLS_VERSION) { + schedulePendingIncomingParts(context); + } + + if (lastSeenVersion < REDPHONE_SUPPORT_VERSION) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new RefreshAttributesJob()); + ApplicationContext.getInstance(context) + .getJobManager() + .add(new DirectoryRefreshJob(false)); + } + + if (lastSeenVersion < PROFILES) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new DirectoryRefreshJob(false)); + } + + if (lastSeenVersion < SCREENSHOTS) { + boolean screenSecurity = PreferenceManager.getDefaultSharedPreferences(context).getBoolean(TextSecurePreferences.SCREEN_SECURITY_PREF, true); + TextSecurePreferences.setScreenSecurityEnabled(context, screenSecurity); + } + + if (lastSeenVersion < PERSISTENT_BLOBS) { + File externalDir = context.getExternalFilesDir(null); + + if (externalDir != null && externalDir.isDirectory() && externalDir.exists()) { + for (File blob : externalDir.listFiles()) { + if (blob.exists() && blob.isFile()) blob.delete(); + } + } + } + + if (lastSeenVersion < INTERNALIZE_CONTACTS) { + if (TextSecurePreferences.isPushRegistered(context)) { + TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true); + } + } + + if (lastSeenVersion < SQLCIPHER) { + scheduleMessagesInPushDatabase(context); + } + + if (lastSeenVersion < SQLCIPHER_COMPLETE) { + File file = context.getDatabasePath("messages.db"); + if (file != null && file.exists()) file.delete(); + } + + if (lastSeenVersion < REMOVE_JOURNAL) { + File file = context.getDatabasePath("messages.db-journal"); + if (file != null && file.exists()) file.delete(); + } + + if (lastSeenVersion < REMOVE_CACHE) { + try { + FileUtils.deleteDirectoryContents(context.getCacheDir()); + } catch (IOException e) { + Log.w(TAG, e); + } + } + + if (lastSeenVersion < IMAGE_CACHE_CLEANUP) { + try { + FileUtils.deleteDirectoryContents(context.getExternalCacheDir()); + GlideApp.get(context).clearDiskCache(); + } catch (IOException e) { + Log.w(TAG, e); + } + } + + // This migration became unnecessary after switching away from WorkManager +// if (lastSeenVersion < 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 (lastSeenVersion < COLOR_MIGRATION) { + long startTime = System.currentTimeMillis(); + DatabaseFactory.getRecipientDatabase(context).updateSystemContactColors((name, color) -> { + if (color != null) { + try { + return MaterialColor.fromSerialized(color); + } catch (MaterialColor.UnknownColorException e) { + Log.w(TAG, "Encountered an unknown color during legacy color migration.", e); + return ContactColorsLegacy.generateFor(name); + } + } + return ContactColorsLegacy.generateFor(name); + }); + Log.i(TAG, "Color migration took " + (System.currentTimeMillis() - startTime) + " ms"); + } + + if (lastSeenVersion < UNIDENTIFIED_DELIVERY) { + if (TextSecurePreferences.isMultiDevice(context)) { + Log.i(TAG, "MultiDevice: Disabling UD (will be re-enabled if possible after pending refresh)."); + TextSecurePreferences.setIsUnidentifiedDeliveryEnabled(context, false); + } + + Log.i(TAG, "Scheduling UD attributes refresh."); + ApplicationContext.getInstance(context) + .getJobManager() + .add(new RefreshAttributesJob()); + } + + if (lastSeenVersion < SIGNALING_KEY_DEPRECATION) { + Log.i(TAG, "Scheduling a RefreshAttributesJob to remove the signaling key remotely."); + ApplicationContext.getInstance(context) + .getJobManager() + .add(new RefreshAttributesJob()); + } + } + + @Override + boolean shouldRetry(@NonNull Exception e) { + return e instanceof RetryLaterException; + } + + private void schedulePendingIncomingParts(Context context) { + final AttachmentDatabase attachmentDb = DatabaseFactory.getAttachmentDatabase(context); + final MmsDatabase mmsDb = DatabaseFactory.getMmsDatabase(context); + final List pendingAttachments = DatabaseFactory.getAttachmentDatabase(context).getPendingAttachments(); + + Log.i(TAG, pendingAttachments.size() + " pending parts."); + for (DatabaseAttachment attachment : pendingAttachments) { + final Reader reader = mmsDb.readerFor(mmsDb.getMessage(attachment.getMmsId())); + final MessageRecord record = reader.getNext(); + + if (attachment.hasData()) { + Log.i(TAG, "corrected a pending media part " + attachment.getAttachmentId() + "that already had data."); + attachmentDb.setTransferState(attachment.getMmsId(), attachment.getAttachmentId(), AttachmentDatabase.TRANSFER_PROGRESS_DONE); + } else if (record != null && !record.isOutgoing() && record.isPush()) { + Log.i(TAG, "queuing new attachment download job for incoming push part " + attachment.getAttachmentId() + "."); + ApplicationContext.getInstance(context) + .getJobManager() + .add(new AttachmentDownloadJob(attachment.getMmsId(), attachment.getAttachmentId(), false)); + } + reader.close(); + } + } + + private void scheduleMessagesInPushDatabase(Context context) { + PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(context); + Cursor pushReader = null; + + try { + pushReader = pushDatabase.getPending(); + + while (pushReader != null && pushReader.moveToNext()) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new PushDecryptJob(context, + pushReader.getLong(pushReader.getColumnIndexOrThrow(PushDatabase.ID)))); + } + } finally { + if (pushReader != null) + pushReader.close(); + } + } + + public interface DatabaseUpgradeListener { + void setProgress(int progress, int total); + } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull LegacyMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new LegacyMigrationJob(parameters); + } + } +} diff --git a/src/org/thoughtcrime/securesms/migrations/MigrationCompleteEvent.java b/src/org/thoughtcrime/securesms/migrations/MigrationCompleteEvent.java new file mode 100644 index 0000000000..aa37d9f96f --- /dev/null +++ b/src/org/thoughtcrime/securesms/migrations/MigrationCompleteEvent.java @@ -0,0 +1,14 @@ +package org.thoughtcrime.securesms.migrations; + +public class MigrationCompleteEvent { + + private final int version; + + public MigrationCompleteEvent(int version) { + this.version = version; + } + + public int getVersion() { + return version; + } +} diff --git a/src/org/thoughtcrime/securesms/migrations/MigrationCompleteJob.java b/src/org/thoughtcrime/securesms/migrations/MigrationCompleteJob.java new file mode 100644 index 0000000000..072523927e --- /dev/null +++ b/src/org/thoughtcrime/securesms/migrations/MigrationCompleteJob.java @@ -0,0 +1,71 @@ +package org.thoughtcrime.securesms.migrations; + +import androidx.annotation.NonNull; + +import org.greenrobot.eventbus.EventBus; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobs.BaseJob; + +/** + * A job that should be enqueued last in a series of migrations. When this runs, we know that the + * current set of migrations has been completed. + * + * To avoid confusion around the possibility of multiples of these jobs being enqueued as the + * result of doing multiple migrations, we associate the canonicalVersionCode with the job and + * include that in the event we broadcast out. + */ +public class MigrationCompleteJob extends BaseJob { + + public static final String KEY = "MigrationCompleteJob"; + + private final static String KEY_VERSION = "version"; + + private final int version; + + MigrationCompleteJob(int version) { + this(new Parameters.Builder() + .setQueue(Parameters.MIGRATION_QUEUE_KEY) + .setLifespan(Parameters.IMMORTAL) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + version); + } + + private MigrationCompleteJob(@NonNull Job.Parameters parameters, int version) { + super(parameters); + this.version = version; + } + + @Override + public @NonNull Data serialize() { + return new Data.Builder().putInt(KEY_VERSION, version).build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void onCanceled() { + throw new AssertionError("This job should never fail."); + } + + @Override + protected void onRun() throws Exception { + EventBus.getDefault().postSticky(new MigrationCompleteEvent(version)); + } + + @Override + protected boolean onShouldRetry(@NonNull Exception e) { + return true; + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull MigrationCompleteJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new MigrationCompleteJob(parameters, data.getInt(KEY_VERSION)); + } + } +} diff --git a/src/org/thoughtcrime/securesms/migrations/MigrationJob.java b/src/org/thoughtcrime/securesms/migrations/MigrationJob.java new file mode 100644 index 0000000000..299210114c --- /dev/null +++ b/src/org/thoughtcrime/securesms/migrations/MigrationJob.java @@ -0,0 +1,84 @@ +package org.thoughtcrime.securesms.migrations; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JobLogger; +import org.thoughtcrime.securesms.logging.Log; + +/** + * A base class for jobs that are intended to be used in {@link ApplicationMigrations}. Some + * sensible defaults are provided, as well as enforcement that jobs have the correct queue key, + * never expire, and have at most one instance (to avoid double-migrating). + * + * These jobs can never fail, or else the JobManager will skip over them. As a result, if they are + * neither successful nor retryable, they will crash the app. + */ +abstract class MigrationJob extends Job { + + private static final String TAG = Log.tag(MigrationJob.class); + + MigrationJob(@NonNull Parameters parameters) { + super(parameters.toBuilder() + .setQueue(Parameters.MIGRATION_QUEUE_KEY) + .setMaxInstances(1) + .setLifespan(Parameters.IMMORTAL) + .setMaxAttempts(Parameters.UNLIMITED) + .build()); + } + + @Override + public @NonNull Data serialize() { + return Data.EMPTY; + } + + @Override + public @NonNull Result run() { + try { + performMigration(); + return Result.success(); + } catch (RuntimeException e) { + Log.w(TAG, JobLogger.format(this, "Encountered a runtime exception."), e); + throw e; + } catch (Exception e) { + if (shouldRetry(e)) { + Log.w(TAG, JobLogger.format(this, "Encountered a retryable exception."), e); + return Result.retry(); + } else { + Log.w(TAG, JobLogger.format(this, "Encountered a non-runtime fatal exception."), e); + throw new FailedMigrationError(e); + } + } + } + + @Override + public void onCanceled() { + throw new AssertionError("This job should never fail."); + } + + /** + * @return True if you want the UI to be blocked by a spinner if the user opens the application + * during the migration, otherwise false. + */ + abstract boolean isUiBlocking(); + + /** + * Do the actual work of your migration. + */ + abstract void performMigration() throws Exception; + + /** + * @return True if you should retry this job based on the exception type, otherwise false. + * Returning false will result in a crash and your job being re-run upon app start. + * This could result in a crash loop, but considering that this is for an application + * migration, this is likely preferable to skipping it. + */ + abstract boolean shouldRetry(@NonNull Exception e); + + private static class FailedMigrationError extends Error { + FailedMigrationError(Throwable t) { + super(t); + } + } +} diff --git a/src/org/thoughtcrime/securesms/service/KeyCachingService.java b/src/org/thoughtcrime/securesms/service/KeyCachingService.java index a34800073b..4bfa2f45a7 100644 --- a/src/org/thoughtcrime/securesms/service/KeyCachingService.java +++ b/src/org/thoughtcrime/securesms/service/KeyCachingService.java @@ -35,12 +35,12 @@ import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.ConversationListActivity; -import org.thoughtcrime.securesms.DatabaseUpgradeActivity; import org.thoughtcrime.securesms.DummyActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.InvalidPassphraseException; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; +import org.thoughtcrime.securesms.migrations.ApplicationMigrations; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.util.DynamicLanguage; @@ -114,7 +114,7 @@ public class KeyCachingService extends Service { new AsyncTask() { @Override protected Void doInBackground(Void... params) { - if (!DatabaseUpgradeActivity.isUpdate(KeyCachingService.this)) { + if (!ApplicationMigrations.isUpdate(KeyCachingService.this)) { MessageNotifier.updateNotification(KeyCachingService.this); } return null; diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index b761fb19c1..5b7fc0a42e 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -783,7 +783,7 @@ public class TextSecurePreferences { } public static int getLastVersionCode(Context context) { - return getIntegerPreference(context, LAST_VERSION_CODE_PREF, 0); + return getIntegerPreference(context, LAST_VERSION_CODE_PREF, Util.getCanonicalVersionCode()); } public static void setLastVersionCode(Context context, int versionCode) throws IOException { diff --git a/test/unitTest/java/org/thoughtcrime/securesms/jobs/FastJobStorageTest.java b/test/unitTest/java/org/thoughtcrime/securesms/jobs/FastJobStorageTest.java index 2198a1c26a..0b83a70078 100644 --- a/test/unitTest/java/org/thoughtcrime/securesms/jobs/FastJobStorageTest.java +++ b/test/unitTest/java/org/thoughtcrime/securesms/jobs/FastJobStorageTest.java @@ -7,6 +7,7 @@ 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.Job; import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer; import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec; import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec; @@ -272,6 +273,76 @@ public class FastJobStorageTest { assertEquals("1", jobs.get(0).getId()); } + @Test + public void getPendingJobsWithNoDependenciesInCreatedOrder_migrationJobTakesPrecedence() { + FullSpec plainSpec = new FullSpec(new JobSpec("1", "f1", "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.emptyList()); + FullSpec migrationSpec = new FullSpec(new JobSpec("2", "f2", Job.Parameters.MIGRATION_QUEUE_KEY, 5, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.emptyList()); + + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(plainSpec, migrationSpec))); + subject.init(); + + List jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10); + + assertEquals(1, jobs.size()); + assertEquals("2", jobs.get(0).getId()); + } + + @Test + public void getPendingJobsWithNoDependenciesInCreatedOrder_runningMigrationBlocksNormalJobs() { + FullSpec plainSpec = new FullSpec(new JobSpec("1", "f1", "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.emptyList()); + FullSpec migrationSpec = new FullSpec(new JobSpec("2", "f2", Job.Parameters.MIGRATION_QUEUE_KEY, 5, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true), + Collections.emptyList(), + Collections.emptyList()); + + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(plainSpec, migrationSpec))); + subject.init(); + + List jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10); + + assertEquals(0, jobs.size()); + } + + @Test + public void getPendingJobsWithNoDependenciesInCreatedOrder_runningMigrationBlocksLaterMigrationJobs() { + FullSpec migrationSpec1 = new FullSpec(new JobSpec("1", "f1", Job.Parameters.MIGRATION_QUEUE_KEY, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true), + Collections.emptyList(), + Collections.emptyList()); + FullSpec migrationSpec2 = new FullSpec(new JobSpec("2", "f2", Job.Parameters.MIGRATION_QUEUE_KEY, 5, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.emptyList()); + + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(migrationSpec1, migrationSpec2))); + subject.init(); + + List jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10); + + assertEquals(0, jobs.size()); + } + + @Test + public void getPendingJobsWithNoDependenciesInCreatedOrder_onlyReturnFirstEligibleMigrationJob() { + FullSpec migrationSpec1 = new FullSpec(new JobSpec("1", "f1", Job.Parameters.MIGRATION_QUEUE_KEY, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.emptyList()); + FullSpec migrationSpec2 = new FullSpec(new JobSpec("2", "f2", Job.Parameters.MIGRATION_QUEUE_KEY, 5, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false), + Collections.emptyList(), + Collections.emptyList()); + + FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(migrationSpec1, migrationSpec2))); + 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(); @@ -301,7 +372,6 @@ public class FastJobStorageTest { assertEquals(0, dependencies.size()); } - private JobDatabase noopDatabase() { JobDatabase database = mock(JobDatabase.class);