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!"); } } }