Create a new system for application-level migrations.

master
Greyson Parrelli 2019-07-29 19:02:40 -04:00
parent d3bed549f2
commit d0a9bd4c6d
24 changed files with 937 additions and 493 deletions

View File

@ -261,7 +261,7 @@
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DatabaseUpgradeActivity"
<activity android:name=".migrations.ApplicationMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>

View File

@ -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"/>
<ProgressBar android:id="@+id/indeterminate_progress"
android:layout_width="wrap_content"

View File

@ -17,6 +17,9 @@
<!-- AlbumThumbnailView -->
<string name="AlbumThumbnailView_plus">\+%d</string>
<!-- ApplicationMigrationActivity -->
<string name="ApplicationMigrationActivity__signal_is_updating">Signal is updating...</string>
<!-- ApplicationPreferencesActivity -->
<string name="ApplicationPreferencesActivity_currently_s">Currently: %s</string>
<string name="ApplicationPreferenceActivity_you_havent_set_a_passphrase_yet">You haven\'t set a passphrase yet!</string>
@ -1113,8 +1116,6 @@
<string name="database_migration_activity__this_could_take_a_moment_please_be_patient">This could take a moment. Please be patient, we\'ll notify you when the import is complete.</string>
<string name="database_migration_activity__importing">IMPORTING</string>
<!-- database_upgrade_activity -->
<string name="database_upgrade_activity__updating_database">Updating database...</string>
<string name="import_fragment__import_system_sms_database">Import system SMS database</string>
<string name="import_fragment__import_the_database_from_the_default_system">Import the database from the default system messenger app</string>

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Integer> UPGRADE_VERSIONS = new TreeSet<Integer>() {{
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<Void, Void, Void>() {
@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<Integer, Double, Void>
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<DatabaseAttachment> 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));
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String> constraintKeys = new LinkedList<>();
private long createTime;
private long maxBackoff;
private long lifespan;
private int maxAttempts;
private int maxInstances;
private String queue;
private List<String> 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<String> 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) {

View File

@ -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<EmptyQueueListener> 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(() -> {

View File

@ -15,7 +15,7 @@ public final class FullSpec {
@NonNull List<ConstraintSpec> constraintSpecs,
@NonNull List<DependencySpec> dependencySpecs)
{
this.jobSpec = jobSpec;
this.jobSpec = jobSpec;
this.constraintSpecs = constraintSpecs;
this.dependencySpecs = dependencySpecs;
}

View File

@ -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<JobSpec> getPendingJobsWithNoDependenciesInCreatedOrder(long currentTime) {
return Stream.of(jobs)
.filterNot(JobSpec::isRunning)
.filter(this::firstInQueue)
.filter(j -> !dependenciesByJobId.containsKey(j.getId()) || dependenciesByJobId.get(j.getId()).isEmpty())
.filter(j -> j.getNextRunAttemptTime() <= currentTime)
.sorted((j1, j2) -> Long.compare(j1.getCreateTime(), j2.getCreateTime()))
.toList();
Optional<JobSpec> 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<JobSpec> 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) {

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
});
}
}

View File

@ -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<Boolean> 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<MigrationJob> 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<Boolean> 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<MigrationJob> getMigrationJobs(@NonNull Context context, int lastSeenVersion) {
List<MigrationJob> jobs = new LinkedList<>();
if (lastSeenVersion < Version.LEGACY) {
jobs.add(new LegacyMigrationJob());
}
return jobs;
}
}

View File

@ -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<DatabaseMigrationJob> {
@Override
public @NonNull DatabaseMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new DatabaseMigrationJob(parameters);
}
}
}

View File

@ -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<DatabaseAttachment> 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<LegacyMigrationJob> {
@Override
public @NonNull LegacyMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new LegacyMigrationJob(parameters);
}
}
}

View File

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

View File

@ -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<MigrationCompleteJob> {
@Override
public @NonNull MigrationCompleteJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new MigrationCompleteJob(parameters, data.getInt(KEY_VERSION));
}
}
}

View File

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

View File

@ -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<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
if (!DatabaseUpgradeActivity.isUpdate(KeyCachingService.this)) {
if (!ApplicationMigrations.isUpdate(KeyCachingService.this)) {
MessageNotifier.updateNotification(KeyCachingService.this);
}
return null;

View File

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

View File

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