package org.commcare.models.database.user;
import android.content.Context;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteException;
import net.sqlcipher.database.SQLiteOpenHelper;
import org.commcare.CommCareApplication;
import org.commcare.android.logging.ForceCloseLogEntry;
import org.commcare.cases.ledger.Ledger;
import org.commcare.android.javarosa.AndroidLogEntry;
import org.commcare.android.javarosa.DeviceReportRecord;
import org.commcare.cases.model.Case;
import org.commcare.logging.XPathErrorEntry;
import org.commcare.models.database.AndroidTableBuilder;
import org.commcare.models.database.DbUtil;
import org.commcare.models.database.IndexedFixturePathUtils;
import org.commcare.android.database.user.models.ACase;
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.SessionStateDescriptor;
import org.commcare.modern.database.DatabaseIndexingUtils;
import org.commcare.modern.database.TableBuilder;
import org.javarosa.core.model.User;
import org.javarosa.core.model.instance.FormInstance;
import org.javarosa.core.services.storage.Persistable;
/**
* The helper for opening/updating the user (encrypted) db space for
* CommCare. This stores users, cases, fixtures, form records, etc.
*
* @author ctsims
*/
public class DatabaseUserOpenHelper extends SQLiteOpenHelper {
/**
* Version History
* V.4 - Added Stock table for tracking quantities. Fixed Case ID index
* V.5 - Fixed Ledger Stock ID's
* V.6 - Indexed the case open + case type pairing (~every select screen)
* Added Case Index table and join
* Added Entity Cache Table
* V.7 - Case index models now maintain relationship types. Migration object
* used to update DB
* V.8 - Merge commcare-odk and commcare User, make AUser legacy type.
* V.9 - Update serialized fixtures in db to use new schema
* V.10 - Migration of FormRecord to add appId field
* V.11 - Add table for storing xpath errors for specific cc app versions
* V.12 - Drop old GeocodeCacheModel table
* V.13 - Add tables for storing normal device logs and force close logs in user storage
* V.14 - Change format of last modified date in form record to canonical SQLite form
* V.15 - Add table to store path info about storage-backed fixture tables
* V.16 - Add type -> id index for case index storage
* V.17 - Add global counter metadata field to form records, for use in submission ordering
* V.18 - Add index on @owner_id for cases
*/
private static final int USER_DB_VERSION = 18;
private static final String USER_DB_LOCATOR = "database_sandbox_";
private final Context context;
private final String mUserId;
private byte[] fileMigrationKeySeed = null;
public DatabaseUserOpenHelper(Context context, String userKeyRecordId) {
super(context, getDbName(userKeyRecordId), null, USER_DB_VERSION);
this.context = context;
this.mUserId = userKeyRecordId;
}
public static String getDbName(String sandboxId) {
return USER_DB_LOCATOR + sandboxId;
}
@Override
public void onCreate(SQLiteDatabase database) {
try {
database.beginTransaction();
AndroidTableBuilder builder = new AndroidTableBuilder(ACase.STORAGE_KEY);
builder.addData(new ACase());
builder.setUnique(ACase.INDEX_CASE_ID);
database.execSQL(builder.getTableCreateString());
builder = new AndroidTableBuilder("USER");
builder.addData(new User());
database.execSQL(builder.getTableCreateString());
builder = new AndroidTableBuilder(FormRecord.class);
database.execSQL(builder.getTableCreateString());
builder = new AndroidTableBuilder(SessionStateDescriptor.class);
database.execSQL(builder.getTableCreateString());
builder = new AndroidTableBuilder(DeviceReportRecord.class);
database.execSQL(builder.getTableCreateString());
// add table for dedicated xpath error logging for reporting xpath
// errors on specific cc app builds.
builder = new AndroidTableBuilder(XPathErrorEntry.STORAGE_KEY);
builder.addData(new XPathErrorEntry());
database.execSQL(builder.getTableCreateString());
// Add tables for storing normal device logs and force close logs in user storage
// (as opposed to global storage) whenever possible
builder = new AndroidTableBuilder(AndroidLogEntry.STORAGE_KEY);
builder.addData(new AndroidLogEntry());
database.execSQL(builder.getTableCreateString());
builder = new AndroidTableBuilder(ForceCloseLogEntry.STORAGE_KEY);
builder.addData(new ForceCloseLogEntry());
database.execSQL(builder.getTableCreateString());
builder = new AndroidTableBuilder("fixture");
builder.addFileBackedData(new FormInstance());
database.execSQL(builder.getTableCreateString());
DbUtil.createOrphanedFileTable(database);
IndexedFixturePathUtils.createStorageBackedFixtureIndexTable(database);
builder = new AndroidTableBuilder(Ledger.STORAGE_KEY);
builder.addData(new Ledger());
builder.setUnique(Ledger.INDEX_ENTITY_ID);
database.execSQL(builder.getTableCreateString());
//The uniqueness index should be doing this for us
database.execSQL(DatabaseIndexingUtils.indexOnTableCommand(
"case_id_index", "AndroidCase", TableBuilder.scrubName(Case.INDEX_CASE_ID)));
database.execSQL(DatabaseIndexingUtils.indexOnTableCommand(
"case_type_index", "AndroidCase", TableBuilder.scrubName(Case.INDEX_CASE_TYPE)));
database.execSQL(DatabaseIndexingUtils.indexOnTableCommand(
"case_status_index", "AndroidCase", TableBuilder.scrubName(Case.INDEX_CASE_STATUS)));
database.execSQL(DatabaseIndexingUtils.indexOnTableCommand(
"case_owner_id_index", "AndroidCase", TableBuilder.scrubName(Case.INDEX_OWNER_ID)));
database.execSQL(DatabaseIndexingUtils.indexOnTableCommand(
"case_status_open_index", "AndroidCase", "case_type,case_status"));
database.execSQL(DatabaseIndexingUtils.indexOnTableCommand(
"ledger_entity_id", "ledger", "entity_id"));
DbUtil.createNumbersTable(database);
database.execSQL(EntityStorageCache.getTableDefinition());
EntityStorageCache.createIndexes(database);
database.execSQL(AndroidCaseIndexTable.getTableDefinition());
AndroidCaseIndexTable.createIndexes(database);
database.setVersion(USER_DB_VERSION);
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
}
@Override
public SQLiteDatabase getWritableDatabase(String key) {
fileMigrationKeySeed = key.getBytes();
try {
return super.getWritableDatabase(key);
} catch (SQLiteException sqle) {
DbUtil.trySqlCipherDbUpdate(key, context, getDbName(mUserId));
return super.getWritableDatabase(key);
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
boolean inSenseMode = false;
//TODO: Not a great way to get the current app! Pass this in to the constructor.
//I am preeeeeety sure that we can't get here without _having_ an app/platform, but not 100%
try {
if (CommCareApplication.instance().getCommCarePlatform() != null && CommCareApplication.instance().getCommCarePlatform().getCurrentProfile() != null) {
if (CommCareApplication.instance().getCommCarePlatform().getCurrentProfile() != null &&
CommCareApplication.instance().getCommCarePlatform().getCurrentProfile().isFeatureActive("sense")) {
inSenseMode = true;
}
}
} catch (Exception e) {
}
new UserDatabaseUpgrader(context, mUserId, inSenseMode, fileMigrationKeySeed).upgrade(db, oldVersion, newVersion);
}
public static void buildTable(SQLiteDatabase database,
String tableName,
Persistable dataObject) {
try {
database.beginTransaction();
AndroidTableBuilder builder = new AndroidTableBuilder(tableName);
builder.addData(dataObject);
database.execSQL(builder.getTableCreateString());
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
}
public static void dropTable(SQLiteDatabase database,
String tableName) {
try {
database.beginTransaction();
database.execSQL("DROP TABLE IF EXISTS '" + tableName + "'");
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
}
}