package org.commcare.models.database.user; import android.content.Context; import android.net.Uri; import android.util.Log; import net.sqlcipher.database.SQLiteDatabase; import org.commcare.CommCareApplication; import org.commcare.android.database.user.models.ACasePreV24Model; import org.commcare.android.database.user.models.FormRecordV2; import org.commcare.android.database.user.models.FormRecordV3; import org.commcare.android.database.user.models.SessionStateDescriptor; import org.commcare.android.logging.ForceCloseLogEntry; import org.commcare.android.javarosa.AndroidLogEntry; import org.commcare.cases.model.Case; import org.commcare.cases.model.StorageIndexedTreeElementModel; import org.commcare.logging.XPathErrorEntry; import org.commcare.models.database.user.models.AndroidCaseIndexTablePreV21; import org.commcare.modern.database.TableBuilder; 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.javarosa.core.model.User; import org.javarosa.core.services.Logger; import org.javarosa.core.services.storage.Persistable; import java.util.NoSuchElementException; import java.util.Set; import java.util.Vector; import static org.commcare.modern.database.IndexedFixturePathsConstants.INDEXED_FIXTURE_PATHS_COL_ATTRIBUTES; import static org.commcare.modern.database.IndexedFixturePathsConstants.INDEXED_FIXTURE_PATHS_TABLE; /** * @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) { // A lot of the upgrade processes can take a little while, so we tell the service to wait // longer than usual in order to make sure the upgrade has time to finish CommCareApplication.instance().setCustomServiceBindTimeout(5 * 60 * 1000); int startVersion = oldVersion; 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; } } if (oldVersion == 18) { if (upgradeEighteenNineteen(db)) { oldVersion = 19; } } if (oldVersion == 19) { if (upgradeNineteenTwenty(db)) { oldVersion = 20; } } if (oldVersion == 20) { if (upgradeTwentyTwentyOne(db)) { oldVersion = 21; } } if (oldVersion == 21) { if (upgradeTwentyOneTwentyTwo(db)) { oldVersion = 22; } } if (oldVersion == 22) { if (upgradeTwentyTwoTwentyThree(db)) { oldVersion = 23; } } if (oldVersion == 23) { if (upgradeTwentyThreeTwentyFour(db)) { oldVersion = 24; } } if (oldVersion == 24) { // We are doing migration to v25 because of a bug in migration to v23 earlier, so // we only want to actually trigger this for apps already on v23 or greater if (startVersion > 22) { if (upgradeTwentyFourTwentyFive(db)) { oldVersion = 25; } } else { oldVersion = 25; } } if (oldVersion == 25) { if(upgradeTwentyFiveTwentySix(db)){ oldVersion = 26; } } } 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) { 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(AndroidCaseIndexTablePreV21.getTableDefinition()); AndroidCaseIndexTable.createIndexes(db); AndroidCaseIndexTablePreV21 cit = new AndroidCaseIndexTablePreV21(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) { 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) { long start = System.currentTimeMillis(); db.beginTransaction(); try { SqlStorage<Persistable> userStorage = new SqlStorage<>(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) { 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. TableBuilder builder = new TableBuilder(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 { TableBuilder builder = new TableBuilder(AndroidLogEntry.STORAGE_KEY); builder.addData(new AndroidLogEntry()); db.execSQL(builder.getTableCreateString()); builder = new TableBuilder(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) { db.beginTransaction(); try { SqlStorage<FormRecordV2> formRecordSqlStorage = new SqlStorage<>( FormRecord.STORAGE_KEY, FormRecordV2.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 (FormRecordV2 formRecord : formRecordSqlStorage) { formRecordSqlStorage.write(formRecord); } db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeFourteenFifteen(SQLiteDatabase db) { db.beginTransaction(); try { IndexedFixturePathUtils.createStorageBackedFixtureIndexTableV15(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<FormRecordV3> 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<FormRecordV3> newStorage = new SqlStorage<>( FormRecord.STORAGE_KEY, FormRecordV3.class, new ConcreteAndroidDbHelper(c, db)); for (FormRecordV3 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, ACasePreV24Model.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 boolean upgradeEighteenNineteen(SQLiteDatabase db) { db.beginTransaction(); try { SqlStorage<ACase> caseStorage = new SqlStorage<>(ACase.STORAGE_KEY, ACasePreV24Model.class, new ConcreteAndroidDbHelper(c, db)); AndroidCaseIndexTablePreV21 indexTable = new AndroidCaseIndexTablePreV21(db); indexTable.reIndexAllCases(caseStorage); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeNineteenTwenty(SQLiteDatabase db) { db.beginTransaction(); try { Set<String> allIndexedFixtures = IndexedFixturePathUtils.getAllIndexedFixtureNamesAsSet(db); for (String fixtureName : allIndexedFixtures) { String tableName = StorageIndexedTreeElementModel.getTableName(fixtureName); SqlStorage<StorageIndexedTreeElementModel> storageForThisFixture = new SqlStorage<>(tableName, StorageIndexedTreeElementModel.class, new ConcreteAndroidDbHelper(c, db)); StorageIndexedTreeElementModel exampleChildElement = storageForThisFixture.iterate().nextRecord(); IndexedFixturePathUtils.buildFixtureIndices(db, tableName, exampleChildElement.getIndexColumnNames()); } db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeTwentyTwentyOne(SQLiteDatabase db) { db.beginTransaction(); try { UserDbUpgradeUtils.addRelationshipToAllCases(c, db); UserDbUpgradeUtils.migrateFormRecordsToV3(c, db); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeTwentyOneTwentyTwo(SQLiteDatabase db) { //drop the existing table and recreate using current definition db.beginTransaction(); try { db.execSQL("DROP TABLE IF EXISTS " + EntityStorageCache.TABLE_NAME); db.execSQL(EntityStorageCache.getTableDefinition()); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeTwentyTwoTwentyThree(SQLiteDatabase db) { // drop the existing table and recreate using current definition boolean success; Vector<Uri> migratedInstances; db.beginTransaction(); try { migratedInstances = UserDbUpgradeUtils.migrateV4FormRecords(c, db); db.setTransactionSuccessful(); success = true; } finally { db.endTransaction(); } // delete all instance provider entries if everything good until now if (success && migratedInstances != null) { try { for (Uri instanceUri : migratedInstances) { c.getContentResolver().delete(instanceUri, null, null); } } catch (Exception e) { // Failure here won't cause any problems in app operations. So fail silently. e.printStackTrace(); Logger.exception("Error while deleting InstanceProvider entries during user db migration", e); } } return success; } /** * Add external_id index to Case table */ private boolean upgradeTwentyThreeTwentyFour(SQLiteDatabase db) { db.beginTransaction(); try { db.execSQL(DbUtil.addColumnToTable( ACase.STORAGE_KEY, Case.EXTERNAL_ID_KEY, "TEXT")); SqlStorage<ACase> caseStorage = new SqlStorage<>(ACase.STORAGE_KEY, ACase.class, new ConcreteAndroidDbHelper(c, db)); updateModels(caseStorage); db.execSQL(DatabaseIndexingUtils.indexOnTableCommand( "case_external_id_index", "AndroidCase", "external_id")); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } // check for integrity of SSD records and wipes the whole table in case of any discrepancy private boolean upgradeTwentyFourTwentyFive(SQLiteDatabase db) { db.beginTransaction(); try { boolean strandedRecordObserved = false; SqlStorage<FormRecord> formRecordStorage = UserDbUpgradeUtils.getFormRecordStorage(c, db, FormRecord.class); SqlStorage<SessionStateDescriptor> ssdStorage = new SqlStorage<>( SessionStateDescriptor.STORAGE_KEY, SessionStateDescriptor.class, new ConcreteAndroidDbHelper(c, db)); for (SessionStateDescriptor ssd : ssdStorage) { // we are in invalid state if formRecord with corresponding ssd form id // either doesn't exist or has status unstarted try { FormRecord formRecord = formRecordStorage.read(ssd.getFormRecordId()); if (formRecord.getStatus().contentEquals(FormRecord.STATUS_UNSTARTED)) { strandedRecordObserved = true; break; } } catch (NoSuchElementException e) { strandedRecordObserved = true; break; } } if (strandedRecordObserved) { SqlStorage.wipeTable(db, SessionStateDescriptor.STORAGE_KEY); // Since we have wiped out SSD records, we won't be able to resume // incomplete forms with their earlier session state. Therfore we are // going to delete all incomplete form records as well Vector<FormRecord> incompleteRecords = formRecordStorage.getRecordsForValue(FormRecord.META_STATUS, FormRecord.STATUS_INCOMPLETE); for (FormRecord incompleteRecord : incompleteRecords) { formRecordStorage.remove(incompleteRecord); } } db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private boolean upgradeTwentyFiveTwentySix(SQLiteDatabase db) { db.beginTransaction(); try { db.execSQL(DbUtil.addColumnToTable( INDEXED_FIXTURE_PATHS_TABLE, INDEXED_FIXTURE_PATHS_COL_ATTRIBUTES, "BLOB")); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } } private void migrateV2FormRecordsForSingleApp(String appId, SqlStorage<FormRecordV2> oldStorage, Vector<FormRecordV3> 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)); FormRecordV3 newRecord = new FormRecordV3( 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.updateStatus(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); } } }