package org.commcare.models; import android.util.Log; import org.commcare.CommCareApplication; import org.commcare.models.database.SqlStorage; import org.commcare.android.database.user.models.FormRecord; import org.commcare.android.database.user.models.SessionStateDescriptor; import org.commcare.modern.session.SessionWrapperInterface; import org.commcare.session.CommCareSession; import org.commcare.session.SessionDescriptorUtil; import org.commcare.session.SessionFrame; import org.commcare.suite.model.ComputedDatum; import org.commcare.suite.model.EntityDatum; import org.commcare.suite.model.Entry; import org.commcare.suite.model.FormEntry; import org.commcare.suite.model.SessionDatum; import org.commcare.suite.model.StackOperation; import org.commcare.util.CommCarePlatform; import org.commcare.utils.AndroidInstanceInitializer; import org.commcare.utils.CommCareUtil; import org.javarosa.core.model.condition.EvaluationContext; import java.util.Date; import java.util.Hashtable; import java.util.Vector; import javax.crypto.SecretKey; /** * This is a container class which maintains all of the appropriate hooks for managing the details * of the current "state" of an application (the session, the relevant forms) and the hooks for * manipulating them in a single place. * * @author ctsims */ public class AndroidSessionWrapper implements SessionWrapperInterface { private static final String TAG = AndroidSessionWrapper.class.getSimpleName(); //The state descriptor will need these private final CommCareSession session; private int formRecordId = -1; private int sessionStateRecordId = -1; public AndroidSessionWrapper(CommCarePlatform platform) { session = new CommCareSession(platform); } public AndroidSessionWrapper(CommCareSession session) { this.session = session; } public void loadFromStateDescription(SessionStateDescriptor descriptor) { this.reset(); this.sessionStateRecordId = descriptor.getID(); this.formRecordId = descriptor.getFormRecordId(); SessionDescriptorUtil.loadSessionFromDescriptor(descriptor.getSessionDescriptor(), session); } /** * Clear all local state and return this session to completely fresh */ public void reset() { this.session.clearAllState(); cleanVolatiles(); } /** * Clears out all of the elements of this wrapper which are for an individual traversal. * Includes any cached info (since the casedb might have changed) and the individual id's * and such. */ private void cleanVolatiles() { formRecordId = -1; sessionStateRecordId = -1; //CTS - Added to fix bugs where casedb didn't get renewed between sessions (possibly //we want to "update" the casedb rather than rebuild it, but this is safest for now. initializer = null; } public CommCareSession getSession() { return session; } /** * Lookup the current form record in database using the form record id * * @return FormRecord or null */ public FormRecord getFormRecord() { if (formRecordId == -1) { return null; } SqlStorage<FormRecord> storage = CommCareApplication.instance().getUserStorage(FormRecord.class); return storage.read(formRecordId); } public void setFormRecordId(int formRecordId) { this.formRecordId = formRecordId; } public int getFormRecordId() { return formRecordId; } /** * Search for a saved sessions that has an incomplete form record using the * same case as the one in the current session descriptor. * * @return Descriptor of the first saved session that has has an incomplete * form record with the same case found in the current descriptor; * otherwise null. */ public SessionStateDescriptor getExistingIncompleteCaseDescriptor() { SessionStateDescriptor ssd = SessionStateDescriptor.buildFromSessionWrapper(this); if (!ssd.getSessionDescriptor().contains(SessionFrame.STATE_DATUM_VAL)) { // don't continue if the current session doesn't use a case return null; } SqlStorage<FormRecord> storage = CommCareApplication.instance().getUserStorage(FormRecord.class); SqlStorage<SessionStateDescriptor> sessionStorage = CommCareApplication.instance().getUserStorage(SessionStateDescriptor.class); // TODO: This is really a join situation. Need a way to outline // connections between tables to enable joining // See if this session's unique hash corresponds to any pending forms. Vector<Integer> ids = sessionStorage.getIDsForValue(SessionStateDescriptor.META_DESCRIPTOR_HASH, ssd.getHash()); // Filter for forms which have actually been started. for (int id : ids) { try { int recordId = Integer.valueOf(sessionStorage.getMetaDataFieldForRecord(id, SessionStateDescriptor.META_FORM_RECORD_ID)); if (!storage.exists(recordId)) { sessionStorage.remove(id); Log.d(TAG, "Removing stale ssd record: " + id); continue; } if (FormRecord.STATUS_INCOMPLETE.equals(storage.getMetaDataFieldForRecord(recordId, FormRecord.META_STATUS))) { return sessionStorage.read(id); } } catch (NumberFormatException nfe) { // TODO: Clean up this record } } return null; } public void commitStub() { //TODO: This should now be locked somehow SqlStorage<FormRecord> storage = CommCareApplication.instance().getUserStorage(FormRecord.class); SqlStorage<SessionStateDescriptor> sessionStorage = CommCareApplication.instance().getUserStorage(SessionStateDescriptor.class); SecretKey key = CommCareApplication.instance().createNewSymmetricKey(); //TODO: this has two components which can fail. be able to roll them back FormRecord r = new FormRecord("", FormRecord.STATUS_UNSTARTED, getSession().getForm(), key.getEncoded(), null, new Date(0), CommCareApplication.instance().getCurrentApp().getAppRecord().getApplicationId()); storage.write(r); setFormRecordId(r.getID()); SessionStateDescriptor ssd = SessionStateDescriptor.buildFromSessionWrapper(this); sessionStorage.write(ssd); sessionStateRecordId = ssd.getID(); } public int getSessionDescriptorId() { return sessionStateRecordId; } /** * @return The evaluation context for the current state. */ public EvaluationContext getEvaluationContext() { return session.getEvaluationContext(getIIF()); } /** * @param commandId The id of the command to evaluate against * @return The evaluation context relevant for the provided command id */ public EvaluationContext getEvaluationContext(String commandId) { return session.getEvaluationContext(getIIF(), commandId); } private AndroidInstanceInitializer initializer; public AndroidInstanceInitializer getIIF() { if(initializer == null) { initializer = new AndroidInstanceInitializer(session); } return initializer; } @Override public String getNeededData() { return session.getNeededData(getEvaluationContext()); } @Override public SessionDatum getNeededDatum(Entry entry) { return session.getNeededDatum(entry); } public static AndroidSessionWrapper mockEasiestRoute(CommCarePlatform platform, String formNamespace, String selectedValue) { AndroidSessionWrapper wrapper = null; int curPredicates = -1; Hashtable<String, Entry> menuMap = platform.getMenuMap(); for (String key : menuMap.keySet()) { Entry e = menuMap.get(key); if (!(e.isView() || e.isRemoteRequest()) && formNamespace.equals(((FormEntry)e).getXFormNamespace())) { //We have an entry. Don't worry too much about how we're supposed to get there for now. //The ideal is that we only need one piece of data if (e.getSessionDataReqs().size() == 1) { //This should fit the bill. Single selection. SessionDatum datum = e.getSessionDataReqs().firstElement(); // we only know how to mock a single case selection if (datum instanceof ComputedDatum) { // Allow mocking of routes that need computed data, useful for case creation forms wrapper = new AndroidSessionWrapper(platform); wrapper.session.setCommand(platform.getModuleNameForEntry((FormEntry) e)); wrapper.session.setCommand(e.getCommandId()); wrapper.session.setComputedDatum(wrapper.getEvaluationContext()); } else if (datum instanceof EntityDatum) { EntityDatum entityDatum = (EntityDatum)datum; //The only thing we need to know now is whether we have a better option available int countPredicates = CommCareUtil.countPreds(entityDatum.getNodeset()); if (wrapper == null) { //No previous value! Yay. //Record the degree of specificity of this selection for now (we'll //actually create the wrapper later curPredicates = countPredicates; } else { //There's already a path to this form. Only keep going //if the current choice is less specific if (countPredicates >= curPredicates) { continue; } } wrapper = new AndroidSessionWrapper(platform); wrapper.session.setCommand(platform.getModuleNameForEntry((FormEntry) e)); wrapper.session.setCommand(e.getCommandId()); wrapper.session.setDatum(entityDatum.getDataId(), selectedValue); } } //We don't really have a good thing to do with this yet. For now, just //hope there's another easy path to this form } } return wrapper; } /** * Finish and seal the current session. Run any stack operations mandated by the current entry * and pop a new frame from the stack, if one exists. */ public boolean terminateSession() { // Possible should re-name this one. We no longer go "home" by default. // We might start a new session's frame. // CTS: note, this maybe should just be clearing volitiles either way // (rather than cherry picking this one), but this is necessary to // ensure that stack ops don't re-use the case casedb as the form if the // form modified the case database before stack ops fire initializer = null; // Ok, now we just need to figure out if it's time to go home, or time // to fire up a new session from the stack if (session.finishExecuteAndPop(getEvaluationContext())) { //We just built a new session stack into the session, so we want to keep that, //clear out the internal state vars, though. cleanVolatiles(); return true; } else { //start from scratch reset(); return false; } } /** * Execute a stack action in the current session environment. Note: This action will * always require a fresh jump to the central controller. */ public void executeStackActions(Vector<StackOperation> ops) { session.executeStackOperations(ops, getEvaluationContext()); //regardless of whether we just updated the current stack, we need to //assume our current volatile states are no longer relevant cleanVolatiles(); } }