package org.commcare;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import net.sqlcipher.database.SQLiteDatabase;
import org.commcare.dalvik.BuildConfig;
import org.commcare.engine.references.JavaFileRoot;
import org.commcare.interfaces.AppFilePathBuilder;
import org.commcare.logging.AndroidLogger;
import org.commcare.models.database.AndroidDbHelper;
import org.commcare.models.database.HybridFileBackedSqlHelpers;
import org.commcare.models.database.SqlStorage;
import org.commcare.models.database.UnencryptedHybridFileBackedSqlStorage;
import org.commcare.models.database.app.DatabaseAppOpenHelper;
import org.commcare.android.database.global.models.ApplicationRecord;
import org.commcare.models.framework.Table;
import org.commcare.preferences.CommCarePreferences;
import org.commcare.provider.ProviderUtils;
import org.commcare.resources.model.Resource;
import org.commcare.resources.model.ResourceTable;
import org.commcare.util.CommCarePlatform;
import org.commcare.utils.AndroidCommCarePlatform;
import org.commcare.utils.GlobalConstants;
import org.commcare.utils.MultipleAppsUtil;
import org.commcare.utils.SessionUnavailableException;
import org.commcare.utils.Stylizer;
import org.javarosa.core.reference.InvalidReferenceException;
import org.javarosa.core.reference.ReferenceManager;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.locale.Localization;
import org.javarosa.core.services.storage.Persistable;
import org.javarosa.core.util.UnregisteredLocaleException;
import java.io.File;
/**
* This (awkwardly named!) container is responsible for keeping track of a single
* CommCare "App". It should be able to set up an App, break it back down, and
* maintain all of the code needed to sandbox applications
*
* @author ctsims
*/
public class CommCareApp implements AppFilePathBuilder {
private ApplicationRecord record;
protected JavaFileRoot fileRoot;
private final AndroidCommCarePlatform platform;
private static final String TAG = CommCareApp.class.getSimpleName();
private static final Object lock = new Object();
// This unfortunately can't be managed entirely by the application object,
// so we have to do some here
public static CommCareApp currentSandbox;
private final Object appDbHandleLock = new Object();
private SQLiteDatabase appDatabase;
private static Stylizer mStylizer;
private int resourceState;
public CommCareApp(ApplicationRecord record) {
this.record = record;
// Now, we need to identify the state of the application resources
int[] version = CommCareApplication.instance().getCommCareVersion();
// TODO: Badly coupled
platform = new AndroidCommCarePlatform(version[0], version[1], this);
}
public Stylizer getStylizer() {
return mStylizer;
}
public String storageRoot() {
return CommCareApplication.instance().getAndroidFsRoot() + "app/" + record.getApplicationId() + "/";
}
private void createPaths() {
String[] paths = new String[]{"", GlobalConstants.FILE_CC_INSTALL,
GlobalConstants.FILE_CC_UPGRADE, GlobalConstants.FILE_CC_CACHE,
GlobalConstants.FILE_CC_FORMS, GlobalConstants.FILE_CC_MEDIA,
GlobalConstants.FILE_CC_LOGS, GlobalConstants.FILE_CC_ATTACHMENTS,
GlobalConstants.FILE_CC_DB};
for (String path : paths) {
File f = new File(fsPath(path));
if (!f.exists()) {
f.mkdirs();
}
}
}
@Override
public String fsPath(String relativeSubDir) {
return storageRoot() + relativeSubDir;
}
private void initializeFileRoots() {
synchronized (lock) {
String root = storageRoot();
fileRoot = new JavaFileRoot(root);
String testFileRoot = "jr://file/mytest.file";
// Assertion: There should be _no_ other file roots when we initialize
try {
String testFilePath = ReferenceManager.instance().DeriveReference(testFileRoot).getLocalURI();
String message = "Cannot setup sandbox. An Existing file root is set up, which directs to: " + testFilePath;
Logger.log(AndroidLogger.TYPE_ERROR_DESIGN, message);
throw new IllegalStateException(message);
} catch (InvalidReferenceException ire) {
// Expected.
}
ReferenceManager.instance().addReferenceFactory(fileRoot);
// Double check that things point to the right place?
}
}
public SharedPreferences getAppPreferences() {
return CommCareApplication.instance().getSharedPreferences(getPreferencesFilename(), Context.MODE_PRIVATE);
}
public void setupSandbox() {
setupSandbox(true);
}
/**
* @param createFilePaths True if file paths should be created as usual. False otherwise
*/
public void setupSandbox(boolean createFilePaths) {
synchronized (lock) {
Logger.log(AndroidLogger.TYPE_RESOURCES, "Staging Sandbox: " + record.getApplicationId());
if (currentSandbox != null) {
currentSandbox.teardownSandbox();
}
// general setup
if (createFilePaths) {
createPaths();
}
initializeFileRoots();
currentSandbox = this;
ProviderUtils.setCurrentSandbox(currentSandbox);
}
}
/**
* If the CommCare app being initialized was first installed on this device with pre-Multiple
* Apps build of CommCare, then its ApplicationRecord will have been generated from an
* older format with missing fields. This method serves to fill in those missing fields,
* and is called after initializeApplication, if and only if the ApplicationRecord has just
* been generated from the old format. Once the update for an AppRecord performs once, it will
* not be performed again.
*/
private void updateAppRecord() {
// Set all of the properties of this record that come from the profile
record.setPropertiesFromProfile(getCommCarePlatform().getCurrentProfile());
// The default value this field was set to may be incorrect, so check it
record.setResourcesStatus(areMMResourcesValidated());
// Set this to false so we don't try to update this app record every time we seat it
record.setConvertedByDbUpgrader(false);
// Commit changes
CommCareApplication.instance().getGlobalStorage(ApplicationRecord.class).write(record);
}
public boolean initializeApplication() {
boolean appReady = initializeApplicationHelper();
if (appReady) {
if (record.wasConvertedByDbUpgrader()) {
updateAppRecord();
}
}
return appReady;
}
private boolean initializeApplicationHelper() {
setupSandbox();
ResourceTable global = platform.getGlobalResourceTable();
ResourceTable upgrade = platform.getUpgradeResourceTable();
ResourceTable recovery = platform.getRecoveryTable();
logTable("Global", global);
logTable("Upgrade", upgrade);
logTable("Recovery", recovery);
// See if any of our tables got left in a weird state
if (global.getTableReadiness() == ResourceTable.RESOURCE_TABLE_UNCOMMITED) {
global.rollbackCommits();
logTable("Global after rollback", global);
}
if (upgrade.getTableReadiness() == ResourceTable.RESOURCE_TABLE_UNCOMMITED) {
upgrade.rollbackCommits();
logTable("Upgrade after rollback", upgrade);
}
// See if we got left in the middle of an update
if (global.getTableReadiness() == ResourceTable.RESOURCE_TABLE_UNSTAGED) {
// If so, repair the global table. (Always takes priority over maintaining the update)
global.repairTable(upgrade);
}
Resource profile = global.getResourceWithId(CommCarePlatform.APP_PROFILE_RESOURCE_ID);
if (profile != null && profile.getStatus() == Resource.RESOURCE_STATUS_INSTALLED) {
platform.initialize(global, false);
try {
Localization.setLocale(
getAppPreferences().getString(CommCarePreferences.PREFS_LOCALE_KEY, "default"));
} catch (UnregisteredLocaleException urle) {
Localization.setLocale(Localization.getGlobalLocalizerAdvanced().getAvailableLocales()[0]);
}
initializeStylizer();
try {
HybridFileBackedSqlHelpers.removeOrphanedFiles(buildAndroidDbHelper().getHandle());
} catch (SessionUnavailableException e) {
Logger.log(AndroidLogger.SOFT_ASSERT,
"Unable to get app db handle to clear orphaned files");
}
return true;
}
return false;
}
private static void logTable(String name, ResourceTable table) {
if (BuildConfig.DEBUG) {
// Avoid printing resource tables in production; it's expensive
Log.d(TAG, name + "\n" + table.toString());
}
}
private void initializeStylizer() {
mStylizer = new Stylizer(CommCareApplication.instance().getApplicationContext());
}
public boolean areMMResourcesValidated() {
SharedPreferences appPreferences = getAppPreferences();
return (appPreferences.getBoolean("isValidated", false) ||
appPreferences.getString(CommCarePreferences.CONTENT_VALIDATED, "no").equals(CommCarePreferences.YES));
}
public void setMMResourcesValidated() {
SharedPreferences.Editor editor = getAppPreferences().edit();
editor.putBoolean("isValidated", true);
editor.commit();
record.setResourcesStatus(true);
CommCareApplication.instance().getGlobalStorage(ApplicationRecord.class).write(record);
}
public int getAppResourceState() {
return resourceState;
}
public void setAppResourceState(int resourceState) {
this.resourceState = resourceState;
}
public void teardownSandbox() {
synchronized (lock) {
Logger.log(AndroidLogger.TYPE_RESOURCES, "Tearing down sandbox: " + record.getApplicationId());
ReferenceManager.instance().removeReferenceFactory(fileRoot);
synchronized (appDbHandleLock) {
if (appDatabase != null) {
appDatabase.close();
}
appDatabase = null;
ProviderUtils.setCurrentSandbox(null);
}
}
}
public AndroidCommCarePlatform getCommCarePlatform() {
return platform;
}
public <T extends Persistable> SqlStorage<T> getStorage(Class<T> c) {
return getStorage(c.getAnnotation(Table.class).value(), c);
}
public <T extends Persistable> SqlStorage<T> getStorage(String name, Class<T> c) {
return new SqlStorage<>(name, c, buildAndroidDbHelper());
}
public <T extends Persistable> UnencryptedHybridFileBackedSqlStorage<T> getFileBackedStorage(String name, Class<T> c) {
return new UnencryptedHybridFileBackedSqlStorage<>(name, c, buildAndroidDbHelper(), this);
}
protected AndroidDbHelper buildAndroidDbHelper() {
return new AndroidDbHelper(CommCareApplication.instance().getApplicationContext()) {
@Override
public SQLiteDatabase getHandle() {
synchronized (appDbHandleLock) {
if (appDatabase == null || !appDatabase.isOpen()) {
appDatabase = new DatabaseAppOpenHelper(this.c, record.getApplicationId()).getWritableDatabase("null");
}
return appDatabase;
}
}
};
}
/**
* Initialize all of the properties that an app record should have and update it to the
* installed state.
*/
public void writeInstalled() {
record.setStatus(ApplicationRecord.STATUS_INSTALLED);
record.setResourcesStatus(areMMResourcesValidated());
record.setPropertiesFromProfile(getCommCarePlatform().getCurrentProfile());
CommCareApplication.instance().getGlobalStorage(ApplicationRecord.class).write(record);
}
public String getUniqueId() {
return this.record.getUniqueId();
}
public String getPreferencesFilename() {
return record.getApplicationId();
}
public ApplicationRecord getAppRecord() {
return this.record;
}
/**
* Refreshes this CommCareApp's ApplicationRecord pointer to be to whatever version is
* currently sitting in the db -- should be called whenever an ApplicationRecord is updated
* while its associated app is seated, so that the 2 are not out of sync
*/
public void refreshAppRecord() {
this.record = MultipleAppsUtil.getAppById(this.record.getUniqueId());
}
/**
* For testing purposes only
*/
public static SQLiteDatabase getAppDatabaseForTesting() {
if (BuildConfig.DEBUG) {
return CommCareApplication.instance().getCurrentApp().buildAndroidDbHelper().getHandle();
} else {
throw new RuntimeException("For testing purposes only!");
}
}
}