package org.commcare.models.database.app; import android.content.Context; import android.util.Log; import net.sqlcipher.database.SQLiteDatabase; import org.commcare.engine.resource.AndroidResourceManager; import org.commcare.models.database.AndroidTableBuilder; import org.commcare.models.database.ConcreteAndroidDbHelper; import org.commcare.models.database.DbUtil; import org.commcare.models.database.SqlStorage; import org.commcare.android.database.app.models.UserKeyRecord; import org.commcare.android.database.app.models.UserKeyRecordV1; import org.commcare.models.database.migration.FixtureSerializationMigration; import org.commcare.android.storage.framework.Persisted; import org.commcare.resources.model.Resource; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Vector; /** * @author ctsims */ class AppDatabaseUpgrader { private final Context context; public AppDatabaseUpgrader(Context context) { this.context = context; } public void upgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion == 1) { if (upgradeOneTwo(db)) { oldVersion = 2; } } if (oldVersion == 2) { if (upgradeTwoThree(db)) { oldVersion = 3; } } if (oldVersion == 3) { if (upgradeThreeFour(db)) { oldVersion = 4; } } if (oldVersion == 4) { if (upgradeFourFive(db)) { oldVersion = 5; } } if (oldVersion == 5) { if (upgradeFiveSix(db)) { oldVersion = 6; } } if (oldVersion == 6) { if (upgradeSixSeven(db)) { oldVersion = 7; } } if (oldVersion == 7) { if (upgradeSevenEight(db)) { oldVersion = 8; } } //NOTE: If metadata changes are made to the Resource model, they need to be //managed by changing the TwoThree updater to maintain that metadata. } private boolean upgradeOneTwo(SQLiteDatabase db) { db.beginTransaction(); try { AndroidTableBuilder builder = new AndroidTableBuilder("RECOVERY_RESOURCE_TABLE"); builder.addData(new Resource()); db.execSQL(builder.getTableCreateString()); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeTwoThree(SQLiteDatabase db) { db.beginTransaction(); try { AndroidTableBuilder builder = new AndroidTableBuilder("RECOVERY_RESOURCE_TABLE"); builder.addData(new Resource()); db.execSQL(builder.getTableCreateString()); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeThreeFour(SQLiteDatabase db) { db.beginTransaction(); try { db.execSQL(DatabaseAppOpenHelper.indexOnTableWithPGUIDCommand("global_index_id", "GLOBAL_RESOURCE_TABLE")); db.execSQL(DatabaseAppOpenHelper.indexOnTableWithPGUIDCommand("upgrade_index_id", "UPGRADE_RESOURCE_TABLE")); db.execSQL(DatabaseAppOpenHelper.indexOnTableWithPGUIDCommand("recovery_index_id", "RECOVERY_RESOURCE_TABLE")); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeFourFive(SQLiteDatabase db) { db.beginTransaction(); try { DbUtil.createNumbersTable(db); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } /** * Create temporary upgrade table. Used to check for new updates without * wiping progress from the main upgrade table */ private boolean upgradeFiveSix(SQLiteDatabase db) { db.beginTransaction(); try { AndroidTableBuilder builder = new AndroidTableBuilder(AndroidResourceManager.TEMP_UPGRADE_TABLE_KEY); builder.addData(new Resource()); db.execSQL(builder.getTableCreateString()); String tableCmd = DatabaseAppOpenHelper.indexOnTableWithPGUIDCommand("temp_upgrade_index_id", AndroidResourceManager.TEMP_UPGRADE_TABLE_KEY); db.execSQL(tableCmd); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } /** * Deserialize app fixtures in db using old form instance serialization * scheme, and re-serialize them using the new scheme that preserves * attributes. */ private boolean upgradeSixSeven(SQLiteDatabase db) { Log.d("AppDatabaseUpgrader", "starting app fixture migration"); FixtureSerializationMigration.stageFixtureTables(db); boolean didFixturesMigrate = FixtureSerializationMigration.migrateUnencryptedFixtureDbBytes(db, context); FixtureSerializationMigration.dropTempFixtureTable(db); return didFixturesMigrate; } /** * Add fields to UserKeyRecord to support PIN auth */ private boolean upgradeSevenEight(SQLiteDatabase db) { db.beginTransaction(); try { SqlStorage<Persisted> storage = new SqlStorage<Persisted>( UserKeyRecordV1.STORAGE_KEY, UserKeyRecordV1.class, new ConcreteAndroidDbHelper(context, db)); Vector<UserKeyRecord> migratedRecords = new Vector<>(); for (Persisted record : storage) { UserKeyRecordV1 oldUKR = (UserKeyRecordV1)record; UserKeyRecord newUKR = UserKeyRecord.fromOldVersion(oldUKR); newUKR.setID(oldUKR.getID()); migratedRecords.add(newUKR); } assignActiveRecords(migratedRecords); for (UserKeyRecord record : migratedRecords) { storage.write(record); } db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } /** * UserKeyRecordV1 does not have the 'isActive' field (because it is being introduced with * this migration). Go through and mark 1 UKR per username as active, based upon the following * rules: * -If there is only 1 record for a username, mark it as active * -If there are multiple records for a username, mark the one with the latest validTo date as * active */ private void assignActiveRecords(Vector<UserKeyRecord> migratedRecords) { // First, create a mapping from username --> list of UKRs Map<String, List<UserKeyRecord>> usernamesToRecords = new HashMap<>(); for (UserKeyRecord record : migratedRecords) { String username = record.getUsername(); List<UserKeyRecord> recordsForUsername = usernamesToRecords.get(username); if (recordsForUsername == null) { recordsForUsername = new ArrayList<>(); usernamesToRecords.put(username, recordsForUsername); } recordsForUsername.add(record); } // Then determine which record for each username to mark as active for (String username : usernamesToRecords.keySet()) { List<UserKeyRecord> records = usernamesToRecords.get(username); UserKeyRecord activeRecord; if (records.size() == 1) { // If there is only 1 record for a username, mark it as active activeRecord = records.get(0); } else { // Otherwise, sort the records in decreasing order of validTo date, and then mark // the first one in the list as active Collections.sort(records, new Comparator<UserKeyRecord>() { @Override public int compare(UserKeyRecord lhs, UserKeyRecord rhs) { return lhs.getValidTo().compareTo(rhs.getValidTo()); } }); activeRecord = records.get(0); } activeRecord.setActive(); } } }