package org.commcare.models.database.migration;
import android.content.Context;
import android.util.Log;
import net.sqlcipher.Cursor;
import net.sqlcipher.database.SQLiteDatabase;
import org.commcare.CommCareApplication;
import org.commcare.logging.AndroidLogger;
import org.commcare.models.database.AndroidTableBuilder;
import org.commcare.models.database.ConcreteAndroidDbHelper;
import org.commcare.models.database.DbUtil;
import org.commcare.models.database.HybridFileBackedSqlStorage;
import org.commcare.models.database.SqlStorage;
import org.commcare.models.database.SqlStorageIterator;
import org.commcare.models.database.UnencryptedHybridFileBackedSqlStorage;
import org.javarosa.core.model.instance.FormInstance;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.storage.Persistable;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
/**
* Deserialize all fixtures in a db using old form instance serialization
* scheme, and re-serialize them using the new scheme.
*
* The updated form instance serialization scheme provides more comprehensive
* handling of attributes, preserving datatypes, and other attributes across
* serializations.
*
* Used in app database migration V.7 and user database migration V.9
*
* This code becomes irrelevant once no more devices need to be migrated off of
* 2.24
*
* @author Phillip Mates (pmates@dimagi.com).
*/
public class FixtureSerializationMigration {
private static final String TAG = FixtureSerializationMigration.class.getSimpleName();
public static boolean migrateUnencryptedFixtureDbBytes(SQLiteDatabase db,
Context c) {
return migrateFixtureDbBytes(db, c, null, null);
}
public static boolean migrateFixtureDbBytes(SQLiteDatabase db, Context c,
String directoryName,
byte[] fileMigrationKeySeed) {
// Not sure how long this process should take, so tell the service to
// wait longer to make sure this can finish.
CommCareApplication.instance().setCustomServiceBindTimeout(60 * 5 * 1000);
long start = System.currentTimeMillis();
db.beginTransaction();
ConcreteAndroidDbHelper helper = new ConcreteAndroidDbHelper(c, db);
DataInputStream fixtureByteStream = null;
try {
HybridFileBackedSqlStorage<Persistable> fixtureStorage;
if (fileMigrationKeySeed != null) {
fixtureStorage =
new HybridFileBackedSqlStorageForMigration<Persistable>("fixture", FormInstance.class, helper, directoryName, fileMigrationKeySeed);
} else {
fixtureStorage =
new UnencryptedHybridFileBackedSqlStorage<Persistable>("fixture",
FormInstance.class, helper, CommCareApplication.instance().getCurrentApp());
}
SqlStorage<Persistable> oldUserFixtureStorage =
new SqlStorage<Persistable>("oldfixture", FormInstance.class, helper);
int migratedFixtureCount = 0;
for (SqlStorageIterator i = oldUserFixtureStorage.iterate(false); i.hasMore(); ) {
int id = i.nextID();
migratedFixtureCount++;
Log.d(TAG, "migrating fixture " + migratedFixtureCount);
FormInstance fixture = new FormInstance();
fixtureByteStream =
new DataInputStream(new ByteArrayInputStream(oldUserFixtureStorage.readBytes(id)));
fixture.migrateSerialization(fixtureByteStream, helper.getPrototypeFactory());
fixture.setID(-1);
fixtureStorage.write(fixture);
}
db.setTransactionSuccessful();
return true;
} catch (Exception e) {
// Even if it failed, let the migration think it was successful,
// otherwise the app will crash. It's important to let user get to
// a point where they can sync data and then clear user data and
// restore, which ultimately has the same effect as running the
// fixture serialization migration.
db.setTransactionSuccessful();
Logger.log(AndroidLogger.SOFT_ASSERT, "fixture serialization db migration failed");
Logger.exception(e);
// allow subsequent migrations to be processed. Will potentially
// lead to failure if those migrations make use of fixtures.
return true;
} finally {
if (fixtureByteStream != null) {
try {
fixtureByteStream.close();
} catch (Exception e) {
}
}
db.endTransaction();
long elapse = System.currentTimeMillis() - start;
Log.d(TAG, "Serialized fixture update complete in " + elapse + "ms");
}
}
public static void stageFixtureTables(SQLiteDatabase db) {
db.beginTransaction();
boolean resumingMigration = doesTempFixtureTableExist(db);
try {
DbUtil.createOrphanedFileTable(db);
if (resumingMigration) {
db.execSQL("DROP TABLE IF EXISTS fixture;");
} else {
db.execSQL("ALTER TABLE fixture RENAME TO oldfixture;");
}
// make new fixture db w/ filepath and encryption key columns
AndroidTableBuilder builder = new AndroidTableBuilder("fixture");
builder.addFileBackedData(new FormInstance());
db.execSQL(builder.getTableCreateString());
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
private static boolean doesTempFixtureTableExist(SQLiteDatabase db) {
// "SELECT name FROM sqlite_master WHERE type='table' AND name='oldfixture';";
String whereClause = "type =? AND name =?";
String[] whereArgs = new String[]{
"table",
"oldfixture"
};
Cursor cursor = null;
try {
cursor = db.query("sqlite_master", new String[]{"name"},
whereClause, whereArgs, null, null, null);
return cursor.getCount() > 0;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
public static void dropTempFixtureTable(SQLiteDatabase db) {
db.beginTransaction();
try {
db.execSQL("DROP TABLE oldfixture;");
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
}