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