package org.commcare.models.database.user; import android.content.Context; import android.util.Log; import net.sqlcipher.database.SQLiteDatabase; import org.commcare.CommCareApplication; import org.commcare.android.database.user.models.FormRecordV2; import org.commcare.android.logging.ForceCloseLogEntry; import org.commcare.android.javarosa.AndroidLogEntry; import org.commcare.cases.model.Case; import org.commcare.logging.XPathErrorEntry; import org.commcare.models.database.AndroidTableBuilder; import org.commcare.models.database.ConcreteAndroidDbHelper; import org.commcare.models.database.DbUtil; import org.commcare.models.database.IndexedFixturePathUtils; import org.commcare.models.database.SqlStorage; import org.commcare.models.database.SqlStorageIterator; import org.commcare.models.database.migration.FixtureSerializationMigration; import org.commcare.android.database.user.models.ACase; import org.commcare.android.database.user.models.ACasePreV6Model; import org.commcare.android.database.user.models.AUser; import org.commcare.models.database.user.models.AndroidCaseIndexTable; import org.commcare.models.database.user.models.EntityStorageCache; import org.commcare.android.database.user.models.FormRecord; import org.commcare.android.database.user.models.FormRecordV1; import org.commcare.android.database.user.models.GeocodeCacheModel; import org.commcare.modern.database.DatabaseIndexingUtils; import org.commcare.modern.database.TableBuilder; import org.javarosa.core.model.User; import org.javarosa.core.services.storage.Persistable; import java.util.Set; import java.util.Vector; /** * @author ctsims */ class UserDatabaseUpgrader { private static final String TAG = UserDatabaseUpgrader.class.getSimpleName(); private boolean inSenseMode = false; private final Context c; private final byte[] fileMigrationKey; private final String userKeyRecordId; public UserDatabaseUpgrader(Context c, String userKeyRecordId, boolean inSenseMode, byte[] fileMigrationKey) { this.c = c; this.userKeyRecordId = userKeyRecordId; this.inSenseMode = inSenseMode; this.fileMigrationKey = fileMigrationKey; } 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; } } if (oldVersion == 8) { if (upgradeEightNine(db)) { oldVersion = 9; } } if (oldVersion == 9) { if (upgradeNineTen(db)) { oldVersion = 10; } } if (oldVersion == 10) { if (upgradeTenEleven(db)) { oldVersion = 11; } } if (oldVersion == 11) { if (upgradeElevenTwelve(db)) { oldVersion = 12; } } if (oldVersion == 12) { if (upgradeTwelveThirteen(db)) { oldVersion = 13; } } if (oldVersion == 13) { if (upgradeThirteenFourteen(db)) { oldVersion = 14; } } if (oldVersion == 14) { if (upgradeFourteenFifteen(db)) { oldVersion = 15; } } if (oldVersion == 15) { if (upgradeFifteenSixteen(db)) { oldVersion = 16; } } if (oldVersion == 16) { if (upgradeSixteenSeventeen(db)) { oldVersion = 17; } } if (oldVersion == 17) { if (upgradeSeventeenEighteen(db)) { oldVersion = 18; } } } private boolean upgradeOneTwo(final SQLiteDatabase db) { db.beginTransaction(); try { markSenseIncompleteUnsent(db); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeTwoThree(final SQLiteDatabase db) { db.beginTransaction(); try { markSenseIncompleteUnsent(db); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeThreeFour(SQLiteDatabase db) { db.beginTransaction(); try { UserDbUpgradeUtils.addStockTable(db); UserDbUpgradeUtils.updateIndexes(db); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeFourFive(SQLiteDatabase db) { db.beginTransaction(); try { db.execSQL(DatabaseIndexingUtils.indexOnTableCommand("ledger_entity_id", "ledger", "entity_id")); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeFiveSix(SQLiteDatabase db) { //On some devices this process takes a significant amount of time (sorry!) we should //tell the service to wait longer to make sure this can finish. CommCareApplication.instance().setCustomServiceBindTimeout(60 * 5 * 1000); db.beginTransaction(); try { db.execSQL(DatabaseIndexingUtils.indexOnTableCommand("case_status_open_index", "AndroidCase", "case_type,case_status")); DbUtil.createNumbersTable(db); db.execSQL(EntityStorageCache.getTableDefinition()); EntityStorageCache.createIndexes(db); db.execSQL(AndroidCaseIndexTable.getTableDefinition()); AndroidCaseIndexTable.createIndexes(db); AndroidCaseIndexTable cit = new AndroidCaseIndexTable(db); //NOTE: Need to use the PreV6 case model any time we manipulate cases in this model for upgraders //below 6 SqlStorage<ACase> caseStorage = new SqlStorage<ACase>(ACase.STORAGE_KEY, ACasePreV6Model.class, new ConcreteAndroidDbHelper(c, db)); for (ACase c : caseStorage) { cit.indexCase(c); } db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeSixSeven(SQLiteDatabase db) { //On some devices this process takes a significant amount of time (sorry!) we should //tell the service to wait longer to make sure this can finish. CommCareApplication.instance().setCustomServiceBindTimeout(60 * 5 * 1000); long start = System.currentTimeMillis(); db.beginTransaction(); try { SqlStorage<ACase> caseStorage = new SqlStorage<ACase>(ACase.STORAGE_KEY, ACasePreV6Model.class, new ConcreteAndroidDbHelper(c, db)); updateModels(caseStorage); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); Log.d(TAG, "Case model update complete in " + (System.currentTimeMillis() - start) + "ms"); } } /** * Depcrecate the old AUser object so that both platforms are using the User object * to represents users */ private boolean upgradeSevenEight(SQLiteDatabase db) { //On some devices this process takes a significant amount of time (sorry!) we should //tell the service to wait longer to make sure this can finish. CommCareApplication.instance().setCustomServiceBindTimeout(60 * 5 * 1000); long start = System.currentTimeMillis(); db.beginTransaction(); try { SqlStorage<Persistable> userStorage = new SqlStorage<Persistable>(AUser.STORAGE_KEY, AUser.class, new ConcreteAndroidDbHelper(c, db)); SqlStorageIterator<Persistable> iterator = userStorage.iterate(); while (iterator.hasMore()) { AUser oldUser = (AUser)iterator.next(); User newUser = oldUser.toNewUser(); userStorage.write(newUser); } db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); Log.d(TAG, "Case model update complete in " + (System.currentTimeMillis() - start) + "ms"); } } /* * Deserialize user fixtures in db using old form instance serialization * scheme, and re-serialize them using the new scheme that preserves * attributes. */ private boolean upgradeEightNine(SQLiteDatabase db) { Log.d(TAG, "starting user fixture migration"); FixtureSerializationMigration.stageFixtureTables(db); boolean didFixturesMigrate = FixtureSerializationMigration.migrateFixtureDbBytes(db, c, userKeyRecordId, fileMigrationKey); FixtureSerializationMigration.dropTempFixtureTable(db); return didFixturesMigrate; } /** * Adding an appId field to FormRecords, for compatibility with multiple apps functionality */ private boolean upgradeNineTen(SQLiteDatabase db) { // This process could take a while, so tell the service to wait longer to make sure // it can finish CommCareApplication.instance().setCustomServiceBindTimeout(60 * 5 * 1000); db.beginTransaction(); try { if (UserDbUpgradeUtils.multipleInstalledAppRecords()) { // Cannot migrate FormRecords once this device has already started installing // multiple applications, because there is no way to know which of those apps the // existing FormRecords belong to UserDbUpgradeUtils.deleteExistingFormRecordsAndWarnUser(c, db); UserDbUpgradeUtils.addAppIdColumnToTable(db); db.setTransactionSuccessful(); return true; } SqlStorage<FormRecordV1> oldStorage = new SqlStorage<>( FormRecord.STORAGE_KEY, FormRecordV1.class, new ConcreteAndroidDbHelper(c, db)); String appId = UserDbUpgradeUtils.getInstalledAppRecord().getApplicationId(); Vector<FormRecordV2> upgradedRecords = new Vector<>(); // Create all of the updated records, based upon the existing ones for (FormRecordV1 oldRecord : oldStorage) { FormRecordV2 newRecord = new FormRecordV2( oldRecord.getInstanceURIString(), oldRecord.getStatus(), oldRecord.getFormNamespace(), oldRecord.getAesKey(), oldRecord.getInstanceID(), oldRecord.lastModified(), appId); newRecord.setID(oldRecord.getID()); upgradedRecords.add(newRecord); } UserDbUpgradeUtils.addAppIdColumnToTable(db); // Write all of the new records to the updated table SqlStorage<FormRecordV2> newStorage = new SqlStorage<>( FormRecord.STORAGE_KEY, FormRecordV2.class, new ConcreteAndroidDbHelper(c, db)); for (FormRecordV2 r : upgradedRecords) { newStorage.write(r); } db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeTenEleven(SQLiteDatabase db) { db.beginTransaction(); try { // add table for dedicated xpath error logging for reporting xpath // errors on specific cc app builds. AndroidTableBuilder builder = new AndroidTableBuilder(XPathErrorEntry.STORAGE_KEY); builder.addData(new XPathErrorEntry()); db.execSQL(builder.getTableCreateString()); db.setTransactionSuccessful(); return true; } catch (Exception e) { return false; } finally { db.endTransaction(); } } private boolean upgradeElevenTwelve(SQLiteDatabase db) { db.beginTransaction(); try { db.execSQL("DROP TABLE IF EXISTS " + GeocodeCacheModel.STORAGE_KEY); db.setTransactionSuccessful(); } finally { db.endTransaction(); } return true; } private boolean upgradeTwelveThirteen(SQLiteDatabase db) { db.beginTransaction(); try { AndroidTableBuilder builder = new AndroidTableBuilder(AndroidLogEntry.STORAGE_KEY); builder.addData(new AndroidLogEntry()); db.execSQL(builder.getTableCreateString()); builder = new AndroidTableBuilder(ForceCloseLogEntry.STORAGE_KEY); builder.addData(new ForceCloseLogEntry()); db.execSQL(builder.getTableCreateString()); db.setTransactionSuccessful(); return true; } catch (Exception e) { return false; } finally { db.endTransaction(); } } private boolean upgradeThirteenFourteen(SQLiteDatabase db) { // This process could take a while, so tell the service to wait longer // to make sure it can finish CommCareApplication.instance().setCustomServiceBindTimeout(60 * 5 * 1000); db.beginTransaction(); try { SqlStorage<FormRecord> formRecordSqlStorage = new SqlStorage<>( FormRecord.STORAGE_KEY, FormRecord.class, new ConcreteAndroidDbHelper(c, db)); // Re-store all the form records, forcing new date representation // to be used. Must happen proactively because the date parsing // code was updated to handle new representation for (FormRecord formRecord : formRecordSqlStorage) { formRecordSqlStorage.write(formRecord); } db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeFourteenFifteen(SQLiteDatabase db) { db.beginTransaction(); try { IndexedFixturePathUtils.createStorageBackedFixtureIndexTable(db); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeFifteenSixteen(SQLiteDatabase db) { db.beginTransaction(); try { String typeFirstIndexId = "NAME_TARGET_RECORD"; String typeFirstIndex = "name" + ", " + "case_rec_id" + ", " + "target"; db.execSQL(DatabaseIndexingUtils.indexOnTableCommand(typeFirstIndexId, "case_index_storage", typeFirstIndex)); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } /** * Add a metadata field to all form records for "form number" that will be used for ordering * submissions. Since submissions were previously ordered by the last modified property, * set the new form numbers in this order. */ private boolean upgradeSixteenSeventeen(SQLiteDatabase db) { db.beginTransaction(); try { SqlStorage<FormRecordV2> oldStorage = new SqlStorage<>( FormRecord.STORAGE_KEY, FormRecordV2.class, new ConcreteAndroidDbHelper(c, db)); Set<String> idsOfAppsWithOldFormRecords = UserDbUpgradeUtils.getAppIdsForRecords(oldStorage); Vector<FormRecord> upgradedRecords = new Vector<>(); for (String appId : idsOfAppsWithOldFormRecords) { migrateV2FormRecordsForSingleApp(appId, oldStorage, upgradedRecords); } // Add new column to db and then write all of the new records UserDbUpgradeUtils.addFormNumberColumnToTable(db); SqlStorage<FormRecord> newStorage = new SqlStorage<>( FormRecord.STORAGE_KEY, FormRecord.class, new ConcreteAndroidDbHelper(c, db)); for (FormRecord r : upgradedRecords) { newStorage.write(r); } db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } /** * Add index on owner ID to case db */ private boolean upgradeSeventeenEighteen(SQLiteDatabase db) { db.beginTransaction(); try { db.execSQL(DbUtil.addColumnToTable( ACase.STORAGE_KEY, "owner_id", "TEXT")); SqlStorage<ACase> caseStorage = new SqlStorage<>(ACase.STORAGE_KEY, ACase.class, new ConcreteAndroidDbHelper(c, db)); updateModels(caseStorage); db.execSQL(DatabaseIndexingUtils.indexOnTableCommand( "case_owner_id_index", "AndroidCase", "owner_id")); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private void migrateV2FormRecordsForSingleApp(String appId, SqlStorage<FormRecordV2> oldStorage, Vector<FormRecord> upgradedRecords) { Vector<Integer> recordIds = oldStorage.getIDsForValue(FormRecord.META_APP_ID, appId); // Sort the old record ids by their last modified date, which is how form submission // ordering was previously done UserDbUpgradeUtils.sortRecordsByDate(recordIds, oldStorage); int submissionNumber = 0; for (int i = 0; i < recordIds.size(); i++) { FormRecordV2 oldRecord = oldStorage.read(recordIds.elementAt(i)); FormRecord newRecord = new FormRecord( oldRecord.getInstanceURIString(), oldRecord.getStatus(), oldRecord.getFormNamespace(), oldRecord.getAesKey(), oldRecord.getInstanceID(), oldRecord.lastModified(), oldRecord.getAppId()); String statusOfOldRecord = oldRecord.getStatus(); if (FormRecord.STATUS_COMPLETE.equals(statusOfOldRecord) || FormRecord.STATUS_UNSENT.equals(statusOfOldRecord)) { // By processing the old records in order of their last modified date, we make // sure that we are setting this form numbers in the most accurate order we can newRecord.setFormNumberForSubmissionOrdering(submissionNumber++); } newRecord.setID(oldRecord.getID()); upgradedRecords.add(newRecord); } } private void markSenseIncompleteUnsent(final SQLiteDatabase db) { //Fix for Bug in 2.7.0/1, forms in sense mode weren't being properly marked as complete after entry. if (inSenseMode) { //Get form record storage SqlStorage<FormRecord> storage = new SqlStorage<>(FormRecord.STORAGE_KEY, FormRecord.class, new ConcreteAndroidDbHelper(c, db)); //Iterate through all forms currently saved for (FormRecord record : storage) { //Update forms marked as incomplete with the appropriate status if (FormRecord.STATUS_INCOMPLETE.equals(record.getStatus())) { //update to complete to process/send. storage.write(record.updateInstanceAndStatus(record.getInstanceURI().toString(), FormRecord.STATUS_COMPLETE)); } } } } /** * Reads and rewrites all of the records in a table, generally to adapt an old serialization format to a new * format */ private <T extends Persistable> void updateModels(SqlStorage<T> storage) { for (T t : storage) { storage.write(t); } } }