package org.commcare.models.database.user;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import net.sqlcipher.database.SQLiteDatabase;
import org.commcare.CommCareApp;
import org.commcare.CommCareApplication;
import org.commcare.logging.AndroidLogger;
import org.commcare.android.javarosa.DeviceReportRecord;
import org.commcare.models.database.AndroidDbHelper;
import org.commcare.models.database.SqlStorage;
import org.commcare.android.database.app.models.UserKeyRecord;
import org.commcare.android.database.user.models.FormRecord;
import org.commcare.provider.InstanceProviderAPI;
import org.commcare.provider.InstanceProviderAPI.InstanceColumns;
import org.commcare.utils.FileUtil;
import org.javarosa.core.services.Logger;
import java.io.File;
import java.io.IOException;
/**
* @author ctsims
*/
public class UserSandboxUtils {
public static void migrateData(Context c, CommCareApp app,
UserKeyRecord incomingSandbox, byte[] unwrappedOldKey,
UserKeyRecord newSandbox, byte[] unwrappedNewKey)
throws IOException {
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "Migrating an existing user sandbox for " + newSandbox.getUsername());
String newKeyEncoded = rekeyDB(c, incomingSandbox, newSandbox, unwrappedOldKey, unwrappedNewKey);
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "Database is re-keyed and ready for use. Copying over files now");
//OK, so now we have the Db transitioned. What we need to do now is go through and rekey all of our file references.
final SQLiteDatabase db = new DatabaseUserOpenHelper(CommCareApplication.instance(), newSandbox.getUuid()).getWritableDatabase(newKeyEncoded);
try {
//If we were able to iterate over the users, the key was fine, so let's use it to open our db
AndroidDbHelper dbh = new AndroidDbHelper(c) {
@Override
public SQLiteDatabase getHandle() {
return db;
}
};
//TODO: At some point we should really just encode the existence/operations on files in the record models themselves
//Models with Files: Form Record. Log Record
SqlStorage<DeviceReportRecord> reports = new SqlStorage<>(DeviceReportRecord.STORAGE_KEY, DeviceReportRecord.class, dbh);
migrateDeviceReports(reports, newSandbox);
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "Copied over all of the device reports. Moving on to the form records");
SqlStorage<FormRecord> formRecords = new SqlStorage<>(FormRecord.STORAGE_KEY, FormRecord.class, dbh);
migrateFormRecords(c, formRecords, newSandbox);
} finally {
db.close();
}
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "All form records copied over");
//OK! So we should be all set, here. Mark the new sandbox as ready and the old sandbox as ready for cleanup.
finalizeMigration(app, incomingSandbox, newSandbox);
}
/**
* Make a copy of the incoming sandbox's database and re-key it to use the new key.
*/
private static String rekeyDB(Context c, UserKeyRecord incomingSandbox, UserKeyRecord newSandbox,
byte[] unwrappedOldKey, byte[] unwrappedNewKey)
throws IOException {
File oldDb = c.getDatabasePath(DatabaseUserOpenHelper.getDbName(incomingSandbox.getUuid()));
File newDb = c.getDatabasePath(DatabaseUserOpenHelper.getDbName(newSandbox.getUuid()));
//TODO: Make sure old sandbox is already on newest version?
if (newDb.exists()) {
if (!newDb.delete()) {
throw new IOException("Couldn't clear file location " + newDb.getAbsolutePath() + " for new sandbox database");
}
}
FileUtil.copyFile(oldDb, newDb);
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "Created a copy of the DB for the new sandbox. Re-keying it...");
String oldKeyEncoded = getSqlCipherEncodedKey(unwrappedOldKey);
String newKeyEncoded = getSqlCipherEncodedKey(unwrappedNewKey);
SQLiteDatabase rawDbHandle = SQLiteDatabase.openDatabase(newDb.getAbsolutePath(), oldKeyEncoded, null, SQLiteDatabase.OPEN_READWRITE);
rawDbHandle.execSQL("PRAGMA key = '" + oldKeyEncoded + "';");
rawDbHandle.execSQL("PRAGMA rekey = '" + newKeyEncoded + "';");
rawDbHandle.close();
return newKeyEncoded;
}
private static void migrateDeviceReports(SqlStorage<DeviceReportRecord> reports,
UserKeyRecord newSandbox) throws IOException {
for (DeviceReportRecord r : reports) {
File oldPath = new File(r.getFilePath());
File newPath = FileUtil.getNewFileLocation(oldPath, newSandbox.getUuid(), true);
// Copy to a new location while re-encrypting
FileUtil.copyFile(oldPath, newPath);
}
}
/**
* Form records are sadly a bit more complex. We need to both move all of the files,
* insert a new record in the content provider, and then update the form record.
*/
private static void migrateFormRecords(Context c, SqlStorage<FormRecord> formRecords,
UserKeyRecord newSandbox) throws IOException {
ContentResolver cr = c.getContentResolver();
for (FormRecord record : formRecords) {
Uri instanceURI = record.getInstanceURI();
//some records won't have a uri yet.
if (instanceURI == null) {
continue;
}
ContentValues values = new ContentValues();
//otherwise read and prepare the record
Cursor oldRecord = cr.query(instanceURI, new String[]{InstanceColumns.INSTANCE_FILE_PATH, InstanceColumns.DISPLAY_NAME, InstanceColumns.SUBMISSION_URI, InstanceColumns.JR_FORM_ID, InstanceColumns.STATUS, InstanceColumns.CAN_EDIT_WHEN_COMPLETE, InstanceColumns.LAST_STATUS_CHANGE_DATE, InstanceColumns.DISPLAY_SUBTEXT}, null, null, null);
if (!oldRecord.moveToFirst()) {
throw new IOException("Non existant form record at URI " + instanceURI.toString());
}
values.put(InstanceColumns.DISPLAY_NAME, oldRecord.getString(oldRecord.getColumnIndex(InstanceColumns.DISPLAY_NAME)));
values.put(InstanceColumns.SUBMISSION_URI, oldRecord.getString(oldRecord.getColumnIndex(InstanceColumns.SUBMISSION_URI)));
values.put(InstanceColumns.JR_FORM_ID, oldRecord.getString(oldRecord.getColumnIndex(InstanceColumns.JR_FORM_ID)));
values.put(InstanceColumns.STATUS, oldRecord.getString(oldRecord.getColumnIndex(InstanceColumns.STATUS)));
values.put(InstanceColumns.CAN_EDIT_WHEN_COMPLETE, oldRecord.getString(oldRecord.getColumnIndex(InstanceColumns.CAN_EDIT_WHEN_COMPLETE)));
values.put(InstanceColumns.LAST_STATUS_CHANGE_DATE, oldRecord.getLong(oldRecord.getColumnIndex(InstanceColumns.LAST_STATUS_CHANGE_DATE)));
values.put(InstanceColumns.DISPLAY_SUBTEXT, oldRecord.getString(oldRecord.getColumnIndex(InstanceColumns.DISPLAY_SUBTEXT)));
values.put(InstanceProviderAPI.SANDBOX_MIGRATION_SUBMISSION, true);
//Copy over the other metadata
File oldForm = new File(oldRecord.getString(oldRecord.getColumnIndex(InstanceColumns.INSTANCE_FILE_PATH)));
oldRecord.close();
File oldFolder = oldForm.getParentFile();
//find a new spot for it
File newFolder = FileUtil.getNewFileLocation(oldFolder, newSandbox.getUuid(), true);
FileUtil.copyFileDeep(oldFolder, newFolder);
File newfileToWrite = null;
for (File f : newFolder.listFiles()) {
if (f.getName().equals(oldForm.getName())) {
newfileToWrite = f;
}
}
//ok, new directory totally ready. Create a new instanceURI
values.put(InstanceColumns.INSTANCE_FILE_PATH, newfileToWrite.getAbsolutePath());
Uri newUri = cr.insert(InstanceColumns.CONTENT_URI, values);
record = record.updateInstanceAndStatus(newUri.toString(), record.getStatus());
formRecords.write(record);
}
}
private static void finalizeMigration(CommCareApp app, UserKeyRecord incomingSandbox, UserKeyRecord newSandbox) {
SqlStorage<UserKeyRecord> ukr = app.getStorage(UserKeyRecord.class);
SQLiteDatabase ukrdb = ukr.getAccessLock();
ukrdb.beginTransaction();
try {
incomingSandbox.setType(UserKeyRecord.TYPE_PENDING_DELETE);
ukr.write(incomingSandbox);
newSandbox.setType(UserKeyRecord.TYPE_NORMAL);
ukr.write(newSandbox);
ukrdb.setTransactionSuccessful();
} finally {
ukrdb.endTransaction();
}
}
public static String getSqlCipherEncodedKey(byte[] bytes) {
String hexString = "x\"";
for (byte aByte : bytes) {
String hexDigits = Integer.toHexString(0xFF & aByte).toUpperCase();
while (hexDigits.length() < 2) {
hexDigits = "0" + hexDigits;
}
hexString += hexDigits;
}
hexString = hexString + "\"";
return hexString;
}
public static void purgeSandbox(Context context, CommCareApp app, UserKeyRecord sandbox, byte[] key) {
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "Wiping sandbox " + sandbox.getUuid());
//Ok, three steps here. Wipe files out, wipe database, remove key record
//If the db is gone already, just remove the record and move on (something odd has happened)
if (!context.getDatabasePath(DatabaseUserOpenHelper.getDbName(sandbox.getUuid())).exists()) {
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "Sandbox " + sandbox.getUuid() + " has already been purged. removing the record");
SqlStorage<UserKeyRecord> ukr = app.getStorage(UserKeyRecord.class);
ukr.remove(sandbox);
}
final SQLiteDatabase db = new DatabaseUserOpenHelper(CommCareApplication.instance(), sandbox.getUuid()).getWritableDatabase(getSqlCipherEncodedKey(key));
try {
AndroidDbHelper dbh = new AndroidDbHelper(context) {
@Override
public SQLiteDatabase getHandle() {
return db;
}
};
SqlStorage<DeviceReportRecord> reports = new SqlStorage<>(DeviceReportRecord.STORAGE_KEY, DeviceReportRecord.class, dbh);
//Log records
for (DeviceReportRecord r : reports) {
File oldPath = new File(r.getFilePath());
if (oldPath.exists()) {
FileUtil.deleteFileOrDir(oldPath);
}
}
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "Device Report files removed");
//Form records are sadly a bit more complex. We need to both move all of the files,
//insert a new record in the content provider, and then update the form record.
SqlStorage<FormRecord> formRecords = new SqlStorage<>(FormRecord.STORAGE_KEY, FormRecord.class, dbh);
for (FormRecord record : formRecords) {
Uri formUri = record.getInstanceURI();
if (formUri == null) {
continue;
}
Cursor c = context.getContentResolver().query(formUri, new String[]{InstanceColumns._ID}, null, null, null);
try {
//See if the record is still here
if (c.moveToFirst()) {
//If so, just grab the ID and delete this record (it'll take the files with it)
long id = c.getLong(0);
context.getContentResolver().delete(ContentUris.withAppendedId(InstanceColumns.CONTENT_URI, id), null, null);
}
} finally {
c.close();
}
}
} finally {
db.close();
}
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "All files removed for sandbox. Deleting DB");
context.getDatabasePath(DatabaseUserOpenHelper.getDbName(sandbox.getUuid())).delete();
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "Database is gone. Get rid of this record");
//OK! So we should be all set, here. Mark the new sandbox as ready and the old sandbox as ready for cleanup.
SqlStorage<UserKeyRecord> ukr = app.getStorage(UserKeyRecord.class);
ukr.remove(sandbox);
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "Purge complete");
}
}