Create a new system for application-level migrations.
parent
d3bed549f2
commit
d0a9bd4c6d
|
@ -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"/>
|
||||
|
|
|
@ -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"
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(() -> {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue