Move all files to natural position.

This commit is contained in:
Alan Evans
2020-01-06 10:52:48 -05:00
parent 0df36047e7
commit 9ebe920195
3016 changed files with 6 additions and 36 deletions

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.getUiBlockingMigrationStatus().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,202 @@
package org.thoughtcrime.securesms.migrations;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
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.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 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 int LEGACY_CANONICAL_VERSION = 455;
public static final int CURRENT_VERSION = 8;
private static final class Version {
static final int LEGACY = 1;
static final int RECIPIENT_ID = 2;
static final int RECIPIENT_SEARCH = 3;
static final int RECIPIENT_CLEANUP = 4;
static final int AVATAR_MIGRATION = 5;
static final int UUIDS = 6;
static final int CACHED_ATTACHMENTS = 7;
static final int STICKERS_LAUNCH = 8;
}
/**
* 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 (isLegacyUpdate(context)) {
Log.i(TAG, "Detected the need for a legacy update. Last seen canonical version: " + VersionTracker.getLastSeenVersion(context));
TextSecurePreferences.setAppMigrationVersion(context, 0);
}
if (!isUpdate(context)) {
Log.d(TAG, "Not an update. Skipping.");
return;
}
final int lastSeenVersion = TextSecurePreferences.getAppMigrationVersion(context);
Log.d(TAG, "currentVersion: " + CURRENT_VERSION + ", lastSeenVersion: " + lastSeenVersion);
LinkedHashMap<Integer, MigrationJob> migrationJobs = getMigrationJobs(context, lastSeenVersion);
if (migrationJobs.size() > 0) {
Log.i(TAG, "About to enqueue " + migrationJobs.size() + " migration(s).");
boolean uiBlocking = true;
int uiBlockingVersion = lastSeenVersion;
for (Map.Entry<Integer, MigrationJob> entry : migrationJobs.entrySet()) {
int version = entry.getKey();
MigrationJob job = entry.getValue();
uiBlocking &= job.isUiBlocking();
if (uiBlocking) {
uiBlockingVersion = version;
}
jobManager.add(job);
jobManager.add(new MigrationCompleteJob(version));
}
if (uiBlockingVersion > lastSeenVersion) {
Log.i(TAG, "Migration set is UI-blocking through version " + uiBlockingVersion + ".");
UI_BLOCKING_MIGRATION_RUNNING.setValue(true);
} else {
Log.i(TAG, "Migration set is non-UI-blocking.");
UI_BLOCKING_MIGRATION_RUNNING.setValue(false);
}
final long startTime = System.currentTimeMillis();
final int uiVersion = uiBlockingVersion;
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() + ". (Current: " + CURRENT_VERSION + ")");
if (event.getVersion() > CURRENT_VERSION) {
throw new AssertionError("Received a higher version than the current version? App downgrades are not supported. (received: " + event.getVersion() + ", current: " + CURRENT_VERSION + ")");
}
Log.i(TAG, "Updating last migration version to " + event.getVersion());
TextSecurePreferences.setAppMigrationVersion(context, event.getVersion());
if (event.getVersion() == CURRENT_VERSION) {
Log.i(TAG, "Migration complete. Took " + (System.currentTimeMillis() - startTime) + " ms.");
EventBus.getDefault().unregister(this);
VersionTracker.updateLastSeenVersion(context);
UI_BLOCKING_MIGRATION_RUNNING.setValue(false);
} else if (event.getVersion() >= uiVersion) {
Log.i(TAG, "Version is >= the UI-blocking version. Posting 'false'.");
UI_BLOCKING_MIGRATION_RUNNING.setValue(false);
}
}
});
} else {
Log.d(TAG, "No migrations.");
TextSecurePreferences.setAppMigrationVersion(context, CURRENT_VERSION);
VersionTracker.updateLastSeenVersion(context);
UI_BLOCKING_MIGRATION_RUNNING.setValue(false);
}
}
/**
* @return A {@link LiveData} object that will update with whether or not a UI blocking migration
* is in progress.
*/
public static LiveData<Boolean> getUiBlockingMigrationStatus() {
return UI_BLOCKING_MIGRATION_RUNNING;
}
/**
* @return True if a UI blocking migration is running.
*/
public static boolean isUiBlockingMigrationRunning() {
Boolean value = UI_BLOCKING_MIGRATION_RUNNING.getValue();
return value != null && value;
}
/**
* @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(@NonNull Context context) {
return isLegacyUpdate(context) || TextSecurePreferences.getAppMigrationVersion(context) < CURRENT_VERSION;
}
private static LinkedHashMap<Integer, MigrationJob> getMigrationJobs(@NonNull Context context, int lastSeenVersion) {
LinkedHashMap<Integer, MigrationJob> jobs = new LinkedHashMap<>();
if (lastSeenVersion < Version.LEGACY) {
jobs.put(Version.LEGACY, new LegacyMigrationJob());
}
if (lastSeenVersion < Version.RECIPIENT_ID) {
jobs.put(Version.RECIPIENT_ID, new DatabaseMigrationJob());
}
if (lastSeenVersion < Version.RECIPIENT_SEARCH) {
jobs.put(Version.RECIPIENT_SEARCH, new RecipientSearchMigrationJob());
}
if (lastSeenVersion < Version.RECIPIENT_CLEANUP) {
jobs.put(Version.RECIPIENT_CLEANUP, new DatabaseMigrationJob());
}
if (lastSeenVersion < Version.AVATAR_MIGRATION) {
jobs.put(Version.AVATAR_MIGRATION, new AvatarMigrationJob());
}
if (lastSeenVersion < Version.UUIDS) {
jobs.put(Version.UUIDS, new UuidMigrationJob());
}
if (lastSeenVersion < Version.CACHED_ATTACHMENTS) {
jobs.put(Version.CACHED_ATTACHMENTS, new CachedAttachmentsMigrationJob());
}
if (lastSeenVersion < Version.STICKERS_LAUNCH) {
jobs.put(Version.STICKERS_LAUNCH, new StickerLaunchMigrationJob());
}
return jobs;
}
private static boolean isLegacyUpdate(@NonNull Context context) {
return VersionTracker.getLastSeenVersion(context) < LEGACY_CANONICAL_VERSION;
}
}

View File

@@ -0,0 +1,95 @@
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;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.regex.Pattern;
/**
* Previously, we used a recipient's address as the filename for their avatar. We want to use
* recipientId's instead in preparation for UUIDs.
*/
public class AvatarMigrationJob extends MigrationJob {
public static final String KEY = "AvatarMigrationJob";
private static final String TAG = Log.tag(AvatarMigrationJob.class);
private static final Pattern NUMBER_PATTERN = Pattern.compile("^[0-9\\-+]+$");
AvatarMigrationJob() {
this(new Parameters.Builder().build());
}
private AvatarMigrationJob(@NonNull Parameters parameters) {
super(parameters);
}
@Override
public boolean isUiBlocking() {
return true;
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void performMigration() {
File oldDirectory = new File(context.getFilesDir(), "avatars");
File[] files = oldDirectory.listFiles();
if (files == null) {
Log.w(TAG, "Unable to read directory, and therefore unable to migrate any avatars.");
return;
}
Log.i(TAG, "Preparing to move " + files.length + " avatars.");
for (File file : files) {
try {
if (isValidFileName(file.getName())) {
Recipient recipient = Recipient.external(context, file.getName());
byte[] data = Util.readFully(new FileInputStream(file));
AvatarHelper.setAvatar(context, recipient.getId(), data);
} else {
Log.w(TAG, "Invalid file name! Can't migrate this file. It'll just get deleted.");
}
} catch (IOException e) {
Log.w(TAG, "Failed to copy avatar file. Skipping it.", e);
} finally {
file.delete();
}
}
}
@Override
boolean shouldRetry(@NonNull Exception e) {
return false;
}
private static boolean isValidFileName(@NonNull String name) {
return NUMBER_PATTERN.matcher(name).matches() || GroupUtil.isEncodedGroup(name) || NumberUtil.isValidEmail(name);
}
public static class Factory implements Job.Factory<AvatarMigrationJob> {
@Override
public @NonNull AvatarMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new AvatarMigrationJob(parameters);
}
}
}

View File

@@ -0,0 +1,62 @@
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.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.FileUtils;
import java.io.File;
import java.io.IOException;
public class CachedAttachmentsMigrationJob extends MigrationJob {
private static final String TAG = Log.tag(CachedAttachmentsMigrationJob.class);
public static final String KEY = "CachedAttachmentsMigrationJob";
CachedAttachmentsMigrationJob() {
this(new Parameters.Builder().build());
}
private CachedAttachmentsMigrationJob(@NonNull Parameters parameters) {
super(parameters);
}
@Override
boolean isUiBlocking() {
return false;
}
@Override
void performMigration() {
File externalCacheDir = context.getExternalCacheDir();
if (externalCacheDir == null || !externalCacheDir.exists() || !externalCacheDir.isDirectory()) {
Log.w(TAG, "External Cache Directory either does not exist or isn't a directory. Skipping.");
return;
}
FileUtils.deleteDirectoryContents(context.getExternalCacheDir());
GlideApp.get(context).clearDiskCache();
}
@Override
boolean shouldRetry(@NonNull Exception e) {
return false;
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
public static class Factory implements Job.Factory<CachedAttachmentsMigrationJob> {
@Override
public @NonNull CachedAttachmentsMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new CachedAttachmentsMigrationJob(parameters);
}
}
}

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,311 @@
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.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.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob;
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) {
ApplicationDependencies.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) {
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(false));
}
if (lastSeenVersion < MEDIA_DOWNLOAD_CONTROLS_VERSION) {
schedulePendingIncomingParts(context);
}
if (lastSeenVersion < REDPHONE_SUPPORT_VERSION) {
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(false));
}
if (lastSeenVersion < PROFILES) {
ApplicationDependencies.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) {
FileUtils.deleteDirectoryContents(context.getCacheDir());
}
if (lastSeenVersion < IMAGE_CACHE_CLEANUP) {
FileUtils.deleteDirectoryContents(context.getExternalCacheDir());
GlideApp.get(context).clearDiskCache();
}
// 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.");
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
}
if (lastSeenVersion < SIGNALING_KEY_DEPRECATION) {
Log.i(TAG, "Scheduling a RefreshAttributesJob to remove the signaling key remotely.");
ApplicationDependencies.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() + ".");
ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(attachment.getMmsId(), attachment.getAttachmentId(), false));
}
reader.close();
}
}
private static void scheduleMessagesInPushDatabase(@NonNull Context context) {
PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(context);
JobManager jobManager = ApplicationDependencies.getJobManager();
try (Cursor pushReader = pushDatabase.getPending()) {
while (pushReader != null && pushReader.moveToNext()) {
jobManager.add(new PushDecryptMessageJob(context,
pushReader.getLong(pushReader.getColumnIndexOrThrow(PushDatabase.ID))));
}
}
}
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,85 @@
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 {
Log.i(TAG, "About to run " + getClass().getSimpleName());
performMigration();
return Result.success();
} catch (RuntimeException e) {
Log.w(TAG, JobLogger.format(this, "Encountered a runtime exception."), e);
throw new FailedMigrationError(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. " + getClass().getSimpleName());
}
/**
* @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

@@ -0,0 +1,54 @@
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.impl.NetworkConstraint;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import java.io.IOException;
/**
* We added a column for keeping track of the phone number type ("mobile", "home", etc) to the
* recipient database, and therefore we need to do a directory sync to fill in that column.
*/
public class RecipientSearchMigrationJob extends MigrationJob {
public static final String KEY = "RecipientSearchMigrationJob";
RecipientSearchMigrationJob() {
this(new Job.Parameters.Builder().addConstraint(NetworkConstraint.KEY).build());
}
private RecipientSearchMigrationJob(@NonNull Job.Parameters parameters) {
super(parameters);
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
boolean isUiBlocking() {
return false;
}
@Override
void performMigration() throws Exception {
DirectoryHelper.refreshDirectory(context, false);
}
@Override
boolean shouldRetry(@NonNull Exception e) {
return e instanceof IOException;
}
public static class Factory implements Job.Factory<RecipientSearchMigrationJob> {
@Override
public @NonNull RecipientSearchMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new RecipientSearchMigrationJob(parameters);
}
}
}

View File

@@ -0,0 +1,111 @@
package org.thoughtcrime.securesms.migrations;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.jobs.BaseJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.RegistrationLockData;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
import java.io.IOException;
/**
* Deliberately not a {@link MigrationJob} because it is not something that needs to run at app start.
* This migration can run at anytime.
*/
public final class RegistrationPinV2MigrationJob extends BaseJob {
private static final String TAG = Log.tag(RegistrationPinV2MigrationJob.class);
public static final String KEY = "RegistrationPinV2MigrationJob";
public RegistrationPinV2MigrationJob() {
this(new Parameters.Builder()
.setQueue(KEY)
.setMaxInstances(1)
.addConstraint(NetworkConstraint.KEY)
.setLifespan(Job.Parameters.IMMORTAL)
.setMaxAttempts(Job.Parameters.UNLIMITED)
.build());
}
private RegistrationPinV2MigrationJob(@NonNull Parameters parameters) {
super(parameters);
}
@Override
public @NonNull Data serialize() {
return Data.EMPTY;
}
@Override
protected void onRun() throws IOException, UnauthenticatedResponseException {
if (!FeatureFlags.KBS) {
Log.i(TAG, "Not migrating pin to KBS");
return;
}
if (!TextSecurePreferences.isRegistrationLockEnabled(context)) {
Log.i(TAG, "Registration lock disabled");
return;
}
if (!TextSecurePreferences.hasOldRegistrationLockPin(context)) {
Log.i(TAG, "No old pin to migrate");
return;
}
//noinspection deprecation Only acceptable place to read the old pin.
String registrationLockPin = TextSecurePreferences.getDeprecatedRegistrationLockPin(context);
if (registrationLockPin == null | TextUtils.isEmpty(registrationLockPin)) {
Log.i(TAG, "No old pin to migrate");
return;
}
Log.i(TAG, "Migrating pin to Key Backup Service");
try {
RegistrationLockData registrationPinV2Key = ApplicationDependencies.getKeyBackupService()
.newPinChangeSession()
.setPin(registrationLockPin);
TextSecurePreferences.setRegistrationLockMasterKey(context, registrationPinV2Key, System.currentTimeMillis());
} catch (InvalidPinException e) {
Log.w(TAG, "The V1 pin cannot be migrated.", e);
return;
}
Log.i(TAG, "Pin migrated to Key Backup Service");
}
@Override
protected boolean onShouldRetry(@NonNull Exception e) {
return e instanceof IOException;
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onCanceled() {
}
public static class Factory implements Job.Factory<RegistrationPinV2MigrationJob> {
@Override
public @NonNull RegistrationPinV2MigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new RegistrationPinV2MigrationJob(parameters);
}
}
}

View File

@@ -0,0 +1,73 @@
package org.thoughtcrime.securesms.migrations;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.MultiDeviceStickerPackOperationJob;
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.thoughtcrime.securesms.stickers.BlessedPacks;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class StickerLaunchMigrationJob extends MigrationJob {
public static final String KEY = "StickerLaunchMigrationJob";
StickerLaunchMigrationJob() {
this(new Parameters.Builder().build());
}
private StickerLaunchMigrationJob(@NonNull Parameters parameters) {
super(parameters);
}
@Override
public boolean isUiBlocking() {
return false;
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void performMigration() {
installPack(context, BlessedPacks.ZOZO);
installPack(context, BlessedPacks.BANDIT);
}
@Override
boolean shouldRetry(@NonNull Exception e) {
return false;
}
private static void installPack(@NonNull Context context, @NonNull BlessedPacks.Pack pack) {
JobManager jobManager = ApplicationDependencies.getJobManager();
StickerDatabase stickerDatabase = DatabaseFactory.getStickerDatabase(context);
if (stickerDatabase.isPackAvailableAsReference(pack.getPackId())) {
stickerDatabase.markPackAsInstalled(pack.getPackId(), false);
}
jobManager.add(StickerPackDownloadJob.forInstall(pack.getPackId(), pack.getPackKey(), false));
if (TextSecurePreferences.isMultiDevice(context)) {
jobManager.add(new MultiDeviceStickerPackOperationJob(pack.getPackId(), pack.getPackKey(), MultiDeviceStickerPackOperationJob.Type.INSTALL));
}
}
public static class Factory implements Job.Factory<StickerLaunchMigrationJob> {
@Override
public @NonNull
StickerLaunchMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new StickerLaunchMigrationJob(parameters);
}
}
}

View File

@@ -0,0 +1,97 @@
package org.thoughtcrime.securesms.migrations;
import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import java.io.IOException;
import java.util.UUID;
/**
* Couple migrations steps need to happen after we move to UUIDS.
* - We need to get our own UUID.
* - We need to fetch the new UUID sealed sender cert.
* - We need to do a directory sync so we can guarantee that all active users have UUIDs.
*/
public class UuidMigrationJob extends MigrationJob {
public static final String KEY = "UuidMigrationJob";
private static final String TAG = Log.tag(UuidMigrationJob.class);
UuidMigrationJob() {
this(new Parameters.Builder().addConstraint(NetworkConstraint.KEY).build());
}
private UuidMigrationJob(@NonNull Parameters parameters) {
super(parameters);
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
boolean isUiBlocking() {
return false;
}
@Override
void performMigration() throws Exception {
if (!TextSecurePreferences.isPushRegistered(context) || TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) {
Log.w(TAG, "Not registered! Skipping migration, as it wouldn't do anything.");
return;
}
ensureSelfRecipientExists(context);
fetchOwnUuid(context);
rotateSealedSenderCerts(context);
}
@Override
boolean shouldRetry(@NonNull Exception e) {
return e instanceof IOException;
}
private static void ensureSelfRecipientExists(@NonNull Context context) {
DatabaseFactory.getRecipientDatabase(context).getOrInsertFromE164(TextSecurePreferences.getLocalNumber(context));
}
private static void fetchOwnUuid(@NonNull Context context) throws IOException {
RecipientId self = Recipient.self().getId();
UUID localUuid = ApplicationDependencies.getSignalServiceAccountManager().getOwnUuid();
DatabaseFactory.getRecipientDatabase(context).markRegistered(self, localUuid);
TextSecurePreferences.setLocalUuid(context, localUuid);
}
private static void rotateSealedSenderCerts(@NonNull Context context) throws IOException {
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
byte[] certificate = accountManager.getSenderCertificate();
byte[] legacyCertificate = accountManager.getSenderCertificateLegacy();
TextSecurePreferences.setUnidentifiedAccessCertificate(context, certificate);
TextSecurePreferences.setUnidentifiedAccessCertificateLegacy(context, legacyCertificate);
}
public static class Factory implements Job.Factory<UuidMigrationJob> {
@Override
public @NonNull UuidMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new UuidMigrationJob(parameters);
}
}
}