Support independent application migration versions.

This commit is contained in:
Greyson Parrelli 2019-09-05 14:39:18 -04:00
parent f81c0b448e
commit d1a6582ad7
11 changed files with 158 additions and 63 deletions

View file

@ -36,6 +36,7 @@ import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.gcm.FcmJobService;
@ -69,6 +70,7 @@ import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.PeerConnectionFactory.InitializationOptions;
@ -114,6 +116,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
initializeSecurityProvider();
initializeLogging();
initializeCrashHandling();
initializeFirstEverAppLaunch();
initializeAppDependencies();
initializeJobManager();
initializeApplicationMigrations();
@ -223,7 +226,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
.setConstraintFactories(JobManagerFactories.getConstraintFactories(this))
.setConstraintObservers(JobManagerFactories.getConstraintObservers(this))
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(this)))
.setJobMigrator(new JobMigrator(TextSecurePreferences.getJobManagerVersion(this), 2, JobManagerFactories.getJobMigrations(this)))
.setJobMigrator(new JobMigrator(TextSecurePreferences.getJobManagerVersion(this), JobManager.CURRENT_VERSION, JobManagerFactories.getJobMigrations(this)))
.build());
}
@ -239,6 +242,20 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
ApplicationDependencies.init(this, new ApplicationDependencyProvider(this, new SignalServiceNetworkAccess(this)));
}
private void initializeFirstEverAppLaunch() {
if (TextSecurePreferences.getFirstInstallVersion(this) == -1) {
if (!SQLCipherOpenHelper.databaseFileExists(this)) {
Log.i(TAG, "First ever app launch!");
TextSecurePreferences.setAppMigrationVersion(this, ApplicationMigrations.CURRENT_VERSION);
TextSecurePreferences.setJobManagerVersion(this, JobManager.CURRENT_VERSION);
}
Log.i(TAG, "Setting first install version to " + BuildConfig.CANONICAL_VERSION_CODE);
TextSecurePreferences.setFirstInstallVersion(this, BuildConfig.CANONICAL_VERSION_CODE);
}
}
private void initializeGcmCheck() {
if (TextSecurePreferences.isPushRegistered(this)) {
long nextSetTime = TextSecurePreferences.getFcmTokenLastSetTime(this) + TimeUnit.HOURS.toMillis(6);

View file

@ -30,7 +30,7 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
private static final int STATE_NORMAL = 0;
private static final int STATE_CREATE_PASSPHRASE = 1;
private static final int STATE_PROMPT_PASSPHRASE = 2;
private static final int STATE_UPGRADE_DATABASE = 3;
private static final int STATE_UI_BLOCKING_UPGRADE = 3;
private static final int STATE_PROMPT_PUSH_REGISTRATION = 4;
private static final int STATE_EXPERIENCE_UPGRADE = 5;
private static final int STATE_WELCOME_SCREEN = 6;
@ -133,7 +133,7 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
switch (state) {
case STATE_CREATE_PASSPHRASE: return getCreatePassphraseIntent();
case STATE_PROMPT_PASSPHRASE: return getPromptPassphraseIntent();
case STATE_UPGRADE_DATABASE: return getUpgradeDatabaseIntent();
case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent();
case STATE_WELCOME_SCREEN: return getWelcomeIntent();
case STATE_PROMPT_PUSH_REGISTRATION: return getPushRegistrationIntent();
case STATE_EXPERIENCE_UPGRADE: return getExperienceUpgradeIntent();
@ -146,8 +146,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
return STATE_CREATE_PASSPHRASE;
} else if (locked) {
return STATE_PROMPT_PASSPHRASE;
} else if (ApplicationMigrations.isUpdate(this)) {
return STATE_UPGRADE_DATABASE;
} else if (ApplicationMigrations.isUpdate(this) && ApplicationMigrations.isUiBlockingMigrationRunning()) {
return STATE_UI_BLOCKING_UPGRADE;
} else if (!TextSecurePreferences.hasSeenWelcomeScreen(this)) {
return STATE_WELCOME_SCREEN;
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
@ -167,7 +167,7 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
return getRoutedIntent(PassphrasePromptActivity.class, getIntent());
}
private Intent getUpgradeDatabaseIntent() {
private Intent getUiBlockingUpgradeIntent() {
return getRoutedIntent(ApplicationMigrationActivity.class,
TextSecurePreferences.hasPromptedPushRegistration(this)
? getConversationListIntent()

View file

@ -535,6 +535,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.setVersion(DATABASE_VERSION);
}
public static boolean databaseFileExists(@NonNull Context context) {
return context.getDatabasePath(DATABASE_NAME).exists();
}
private void executeStatements(SQLiteDatabase db, String[] statements) {
for (String statement : statements)
db.execSQL(statement);

View file

@ -261,7 +261,7 @@ class JobController {
JobSpec jobSpec = new JobSpec(job.getId(),
job.getFactoryKey(),
job.getParameters().getQueue(),
job.getParameters().getCreateTime(),
System.currentTimeMillis(),
job.getNextRunAttemptTime(),
job.getRunAttempt(),
job.getParameters().getMaxAttempts(),

View file

@ -33,6 +33,8 @@ public class JobManager implements ConstraintObserver.Notifier {
private static final String TAG = JobManager.class.getSimpleName();
public static final int CURRENT_VERSION = 2;
private final Application application;
private final Configuration configuration;
private final ExecutorService executor;

View file

@ -93,7 +93,7 @@ public class FastJobStorage implements JobStorage {
public synchronized @NonNull List<JobSpec> getPendingJobsWithNoDependenciesInCreatedOrder(long currentTime) {
Optional<JobSpec> migrationJob = getMigrationJob();
if (migrationJob.isPresent() && !migrationJob.get().isRunning()) {
if (migrationJob.isPresent() && !migrationJob.get().isRunning() && migrationJob.get().getNextRunAttemptTime() <= currentTime) {
return Collections.singletonList(migrationJob.get());
} else if (migrationJob.isPresent()) {
return Collections.emptyList();

View file

@ -59,6 +59,7 @@ import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.logsubmit.util.Scrubber;
import org.thoughtcrime.securesms.util.BucketInfo;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
@ -514,8 +515,7 @@ public class SubmitLogFragment extends Fragment {
final StringBuilder builder = new StringBuilder();
builder.append("Time : ").append(System.currentTimeMillis()).append('\n');
builder.append("Device : ")
.append(Build.MANUFACTURER).append(" ")
builder.append("Device : ").append(Build.MANUFACTURER).append(" ")
.append(Build.MODEL).append(" (")
.append(Build.PRODUCT).append(")\n");
builder.append("Android : ").append(VERSION.RELEASE).append(" (")
@ -525,6 +525,7 @@ public class SubmitLogFragment extends Fragment {
builder.append("Memory : ").append(getMemoryUsage(context)).append("\n");
builder.append("Memclass : ").append(getMemoryClass(context)).append("\n");
builder.append("OS Host : ").append(Build.HOST).append("\n");
builder.append("First Version: ").append(TextSecurePreferences.getFirstInstallVersion(context)).append("\n");
builder.append("App : ");
try {
builder.append(pm.getApplicationLabel(pm.getApplicationInfo(context.getPackageName(), 0)))

View file

@ -35,7 +35,7 @@ public class ApplicationMigrationActivity extends BaseActivity {
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
ApplicationMigrations.isUiBlockingMigrationRunning().observe(this, running -> {
ApplicationMigrations.getUiBlockingMigrationStatus().observe(this, running -> {
if (running == null) {
return;
}

View file

@ -6,19 +6,17 @@ 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.database.RecipientDatabase;
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.LinkedList;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Manages application-level migrations.
@ -38,10 +36,14 @@ public class ApplicationMigrations {
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 = 3;
private static final class Version {
static final int LEGACY = 455;
static final int RECIPIENT_ID = 525; // TODO [greyson] USE PROPER APPLICATION VERSION
static final int RECIPIENT_SEARCH = 525; // TODO [greyson] USE PROPER APPLICATION VERSION
static final int LEGACY = 1;
static final int RECIPIENT_ID = 2;
static final int RECIPIENT_SEARCH = 3;
}
/**
@ -50,58 +52,80 @@ public class ApplicationMigrations {
* 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 currentVersion = Util.getCanonicalVersionCode();
final int lastSeenVersion = VersionTracker.getLastSeenVersion(context);
final int lastSeenVersion = TextSecurePreferences.getAppMigrationVersion(context);
Log.d(TAG, "currentVersion: " + CURRENT_VERSION + ", lastSeenVersion: " + lastSeenVersion);
Log.d(TAG, "currentVersion: " + currentVersion + " lastSeenVersion: " + lastSeenVersion);
List<MigrationJob> migrationJobs = getMigrationJobs(context, lastSeenVersion);
LinkedHashMap<Integer, 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);
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) {
Log.i(TAG, "Migration set is UI-blocking.");
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);
}
for (MigrationJob job : migrationJobs) {
jobManager.add(job);
}
jobManager.add(new MigrationCompleteJob(currentVersion));
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() + ".");
Log.i(TAG, "Received MigrationCompleteEvent for version " + event.getVersion() + ". (Current: " + CURRENT_VERSION + ")");
if (event.getVersion() == currentVersion) {
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.postValue(false);
} else {
Log.i(TAG, "Version doesn't match. Looking for " + currentVersion + ", but received " + event.getVersion() + ".");
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.postValue(false);
UI_BLOCKING_MIGRATION_RUNNING.setValue(false);
}
}
@ -109,36 +133,45 @@ public class ApplicationMigrations {
* @return A {@link LiveData} object that will update with whether or not a UI blocking migration
* is in progress.
*/
public static LiveData<Boolean> isUiBlockingMigrationRunning() {
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(Context context) {
int currentVersionCode = Util.getCanonicalVersionCode();
int previousVersionCode = VersionTracker.getLastSeenVersion(context);
return previousVersionCode < currentVersionCode;
public static boolean isUpdate(@NonNull Context context) {
return isLegacyUpdate(context) || TextSecurePreferences.getAppMigrationVersion(context) < CURRENT_VERSION;
}
private static List<MigrationJob> getMigrationJobs(@NonNull Context context, int lastSeenVersion) {
List<MigrationJob> jobs = new LinkedList<>();
private static LinkedHashMap<Integer, MigrationJob> getMigrationJobs(@NonNull Context context, int lastSeenVersion) {
LinkedHashMap<Integer, MigrationJob> jobs = new LinkedHashMap<>();
if (lastSeenVersion < Version.LEGACY) {
jobs.add(new LegacyMigrationJob());
jobs.put(Version.LEGACY, new LegacyMigrationJob());
}
if (lastSeenVersion < Version.RECIPIENT_ID) {
jobs.add(new DatabaseMigrationJob());
jobs.put(Version.RECIPIENT_ID, new DatabaseMigrationJob());
}
if (lastSeenVersion < Version.RECIPIENT_SEARCH) {
jobs.add(new RecipientSearchMigrationJob());
jobs.put(Version.RECIPIENT_SEARCH, new RecipientSearchMigrationJob());
}
return jobs;
}
private static boolean isLegacyUpdate(@NonNull Context context) {
return VersionTracker.getLastSeenVersion(context) < LEGACY_CANONICAL_VERSION;
}
}

View file

@ -189,6 +189,10 @@ public class TextSecurePreferences {
private static final String JOB_MANAGER_VERSION = "pref_job_manager_version";
private static final String APP_MIGRATION_VERSION = "pref_app_migration_version";
private static final String FIRST_INSTALL_VERSION = "pref_first_install_version";
public static boolean isScreenLockEnabled(@NonNull Context context) {
return getBooleanPreference(context, SCREEN_LOCK, false);
}
@ -1128,6 +1132,23 @@ public class TextSecurePreferences {
return getIntegerPreference(contex, JOB_MANAGER_VERSION, 1);
}
public static void setAppMigrationVersion(Context context, int version) {
setIntegerPrefrence(context, APP_MIGRATION_VERSION, version);
}
public static int getAppMigrationVersion(Context context) {
return getIntegerPreference(context, APP_MIGRATION_VERSION, 1);
}
public static void setFirstInstallVersion(Context context, int version) {
setIntegerPrefrence(context, FIRST_INSTALL_VERSION, version);
}
public static int getFirstInstallVersion(Context context) {
return getIntegerPreference(context, FIRST_INSTALL_VERSION, -1);
}
public static void setBooleanPreference(Context context, String key, boolean value) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply();
}

View file

@ -380,6 +380,23 @@ public class FastJobStorageTest {
assertEquals("1", jobs.get(0).getId());
}
@Test
public void getPendingJobsWithNoDependenciesInCreatedOrder_onlyMigrationJobWithAppropriateNextRunTime() {
FullSpec migrationSpec1 = new FullSpec(new JobSpec("1", "f1", Job.Parameters.MIGRATION_QUEUE_KEY, 0, 999, 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);
assertTrue(jobs.isEmpty());
}
@Test
public void deleteJobs_writesToDatabase() {
JobDatabase database = noopDatabase();