/*
* Copyright (C) 2009 JavaRosa
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.odk.collect.android.logic;
import org.javarosa.core.model.FormIndex;
import org.javarosa.core.model.GroupDef;
import org.javarosa.core.model.data.IAnswerData;
import org.javarosa.core.model.instance.FormInstance;
import org.javarosa.form.api.FormEntryCaption;
import org.javarosa.form.api.FormEntryController;
import org.javarosa.form.api.FormEntryPrompt;
import org.odk.collect.android.views.ODKView;
import android.util.Log;
import java.util.ArrayList;
/**
* This class is a wrapper for Javarosa's FormEntryController. In theory, if you wanted to replace
* javarosa as the form engine, you should only need to replace the methods in this file. Also, we
* haven't wrapped every method provided by FormEntryController, only the ones we've needed so far.
* Feel free to add more as necessary.
*
* @author carlhartung
*/
public class FormController {
private static final String t = "FormController";
private FormEntryController mFormEntryController;
public static final boolean STEP_INTO_GROUP = true;
public static final boolean STEP_OVER_GROUP = false;
public FormController(FormEntryController fec) {
mFormEntryController = fec;
}
/**
* returns the event for the current FormIndex.
*
* @return
*/
public int getEvent() {
return mFormEntryController.getModel().getEvent();
}
/**
* returns the event for the given FormIndex.
*
* @param index
* @return
*/
public int getEvent(FormIndex index) {
return mFormEntryController.getModel().getEvent(index);
}
/**
* @return true if current FormIndex is readonly. false otherwise.
*/
public boolean isIndexReadonly() {
return mFormEntryController.getModel().isIndexReadonly();
}
/**
* @return current FormIndex.
*/
public FormIndex getFormIndex() {
return mFormEntryController.getModel().getFormIndex();
}
/**
* Return the langauges supported by the currently loaded form.
*
* @return Array of Strings containing the languages embedded in the XForm.
*/
public String[] getLanguages() {
return mFormEntryController.getModel().getLanguages();
}
/**
* @return A String containing the title of the current form.
*/
public String getFormTitle() {
return mFormEntryController.getModel().getFormTitle();
}
/**
* @return the currently selected language.
*/
public String getLanguage() {
return mFormEntryController.getModel().getLanguage();
}
/**
* @return an array of FormEntryCaptions for the current FormIndex. This is how we get group
* information Group 1 > Group 2> etc... The element at [size-1] is the current question
* text, with group names decreasing in hierarchy until array element at [0] is the root
*/
public FormEntryCaption[] getCaptionHierarchy() {
return mFormEntryController.getModel().getCaptionHierarchy();
}
/**
* Returns a caption prompt for the given index. This is used to create a multi-question per
* screen view.
*
* @param index
* @return
*/
public FormEntryCaption getCaptionPrompt(FormIndex index) {
return mFormEntryController.getModel().getCaptionPrompt(index);
}
/**
* Return the caption for the current FormIndex. This is usually used for a repeat prompt.
*
* @return
*/
public FormEntryCaption getCaptionPrompt() {
return mFormEntryController.getModel().getCaptionPrompt();
}
/**
* TODO: We need a good description of what this does, exactly, and why.
*
* @return
*/
public boolean postProcessInstance() {
return mFormEntryController.getModel().getForm().postProcessInstance();
}
/**
* TODO: We need a good description of what this does, exactly, and why.
*
* @return
*/
public FormInstance getInstance() {
return mFormEntryController.getModel().getForm().getInstance();
}
/**
* A convenience method for determining if the current FormIndex is in a group that is/should be
* displayed as a multi-question view. This is useful for returning from the formhierarchy view
* to a selected index.
*
* @param index
* @return
*/
private boolean groupIsFieldList(FormIndex index) {
// if this isn't a group, return right away
if (!(mFormEntryController.getModel().getForm().getChild(index) instanceof GroupDef)) {
return false;
}
GroupDef gd = (GroupDef) mFormEntryController.getModel().getForm().getChild(index); // exceptions?
return (ODKView.FIELD_LIST.equalsIgnoreCase(gd.getAppearanceAttr()));
}
/**
* Tests if the FormIndex 'index' is located inside a group that is marked as a "field-list"
*
* @param index
* @return true if index is in a "field-list". False otherwise.
*/
public boolean indexIsInFieldList(FormIndex index) {
int event = mFormEntryController.getModel().getEvent(index);
if (event == FormEntryController.EVENT_QUESTION) {
// caption[0..len-1]
// caption[len-1] == the question itself
// caption[len-2] == the first group it is contained in.
FormEntryCaption[] captions = mFormEntryController.getModel().getCaptionHierarchy();
if (captions.length < 2) {
// no group
return false;
}
FormEntryCaption grp = captions[captions.length - 2];
return groupIsFieldList(grp.getIndex());
} else if (event == FormEntryController.EVENT_GROUP) {
return groupIsFieldList(index);
} else {
// right now we only test Questions and Groups. Should we also handle
// repeats?
return false;
}
}
/**
* Tests if the current FormIndex is located inside a group that is marked as a "field-list"
*
* @return true if index is in a "field-list". False otherwise.
*/
public boolean indexIsInFieldList() {
return indexIsInFieldList(mFormEntryController.getModel().getFormIndex());
}
/**
* Attempts to save answer at the current FormIndex into the data model.
*
* @param data
* @return
*/
public int answerQuestion(IAnswerData data) {
return mFormEntryController.answerQuestion(data);
}
/**
* Attempts to save answer into the given FormIndex into the data model.
*
* @param index
* @param data
* @return
*/
public int answerQuestion(FormIndex index, IAnswerData data) {
return mFormEntryController.answerQuestion(index, data);
}
/**
* saveAnswer attempts to save the current answer into the data model without doing any
* constraint checking. Only use this if you know what you're doing. For normal form filling you
* should always use answerQuestion or answerCurrentQuestion.
*
* @param index
* @param data
* @return true if saved successfully, false otherwise.
*/
public boolean saveAnswer(FormIndex index, IAnswerData data) {
return mFormEntryController.saveAnswer(index, data);
}
/**
* saveAnswer attempts to save the current answer into the data model without doing any
* constraint checking. Only use this if you know what you're doing. For normal form filling you
* should always use answerQuestion().
*
* @param index
* @param data
* @return true if saved successfully, false otherwise.
*/
public boolean saveAnswer(IAnswerData data) {
return mFormEntryController.saveAnswer(data);
}
/**
* Navigates forward in the form.
*
* @return the next event that should be handled by a view.
*/
public int stepToNextEvent(boolean stepOverGroup) {
if (mFormEntryController.getModel().getEvent() == FormEntryController.EVENT_GROUP
&& indexIsInFieldList() && stepOverGroup) {
return stepOverGroup();
} else {
return mFormEntryController.stepToNextEvent();
}
}
/**
* If using a view like HierarchyView that doesn't support multi-question per screen, step over
* the group represented by the FormIndex.
*
* @return
*/
private int stepOverGroup() {
ArrayList<FormIndex> indicies = new ArrayList<FormIndex>();
GroupDef gd =
(GroupDef) mFormEntryController.getModel().getForm()
.getChild(mFormEntryController.getModel().getFormIndex());
FormIndex idxChild =
mFormEntryController.getModel().incrementIndex(
mFormEntryController.getModel().getFormIndex(), true); // descend into group
for (int i = 0; i < gd.getChildren().size(); i++) {
indicies.add(idxChild);
// don't descend
idxChild = mFormEntryController.getModel().incrementIndex(idxChild, false);
}
// jump to the end of the group
mFormEntryController.jumpToIndex(indicies.get(indicies.size() - 1));
return stepToNextEvent(STEP_OVER_GROUP);
}
/**
* Navigates backward in the form.
*
* @return the event that should be handled by a view.
*/
public int stepToPreviousEvent() {
/*
* Right now this will always skip to the beginning of a group if that group is represented
* as a 'field-list'. Should a need ever arise to step backwards by only one step in a
* 'field-list', this method will have to be updated.
*/
mFormEntryController.stepToPreviousEvent();
// If after we've stepped, we're in a field-list, jump back to the beginning of the group
//
if (indexIsInFieldList()
&& mFormEntryController.getModel().getEvent() == FormEntryController.EVENT_QUESTION) {
// caption[0..len-1]
// caption[len-1] == the question itself
// caption[len-2] == the first group it is contained in.
FormEntryCaption[] captions = mFormEntryController.getModel().getCaptionHierarchy();
FormEntryCaption grp = captions[captions.length - 2];
return mFormEntryController.jumpToIndex(grp.getIndex());
}
return mFormEntryController.getModel().getEvent();
}
/**
* Jumps to a given FormIndex.
*
* @param index
* @return EVENT for the specified Index.
*/
public int jumpToIndex(FormIndex index) {
return mFormEntryController.jumpToIndex(index);
}
/**
* Creates a new repeated instance of the group referenced by the specified FormIndex.
*
* @param questionIndex
*/
public void newRepeat(FormIndex questionIndex) {
mFormEntryController.newRepeat(questionIndex);
}
/**
* Creates a new repeated instance of the group referenced by the current FormIndex.
*
* @param questionIndex
*/
public void newRepeat() {
mFormEntryController.newRepeat();
}
/**
* If the current FormIndex is within a repeated group, will find the innermost repeat, delete
* it, and jump the FormEntryController to the previous valid index. That is, if you have group1
* (2) > group2 (3) and you call deleteRepeat, it will delete the 3rd instance of group2.
*/
public void deleteRepeat() {
FormIndex fi = mFormEntryController.deleteRepeat();
mFormEntryController.jumpToIndex(fi);
}
/**
* Sets the current language.
*
* @param language
*/
public void setLanguage(String language) {
mFormEntryController.setLanguage(language);
}
/**
* Returns an array of question promps.
*
* @return
*/
public FormEntryPrompt[] getQuestionPrompts() throws RuntimeException {
ArrayList<FormIndex> indicies = new ArrayList<FormIndex>();
FormIndex currentIndex = mFormEntryController.getModel().getFormIndex();
// For questions, there is only one.
// For groups, there could be many, but we set that below
FormEntryPrompt[] questions = new FormEntryPrompt[1];
if (mFormEntryController.getModel().getForm().getChild(currentIndex) instanceof GroupDef) {
GroupDef gd =
(GroupDef) mFormEntryController.getModel().getForm().getChild(currentIndex);
// descend into group
FormIndex idxChild = mFormEntryController.getModel().incrementIndex(currentIndex, true);
for (int i = 0; i < gd.getChildren().size(); i++) {
indicies.add(idxChild);
// don't descend
idxChild = mFormEntryController.getModel().incrementIndex(idxChild, false);
}
// we only display relevant questions
ArrayList<FormEntryPrompt> questionList = new ArrayList<FormEntryPrompt>();
for (int i = 0; i < indicies.size(); i++) {
FormIndex index = indicies.get(i);
if (mFormEntryController.getModel().getEvent(index) != FormEntryController.EVENT_QUESTION) {
String errorMsg =
"Only questions are allowed in 'field-list'. Bad node is: "
+ index.getReference().toString(false);
RuntimeException e = new RuntimeException(errorMsg);
Log.e(t, errorMsg);
throw e;
}
// we only display relevant questions
if (mFormEntryController.getModel().isIndexRelevant(index)) {
questionList.add(mFormEntryController.getModel().getQuestionPrompt(index));
}
}
questions = new FormEntryPrompt[questionList.size()];
questionList.toArray(questions);
} else {
// We have a quesion, so just get the one prompt
questions[0] = mFormEntryController.getModel().getQuestionPrompt();
}
return questions;
}
public FormEntryPrompt getQuestionPrompt(FormIndex index) {
return mFormEntryController.getModel().getQuestionPrompt(index);
}
public FormEntryPrompt getQuestionPrompt() {
return mFormEntryController.getModel().getQuestionPrompt();
}
/**
* Returns an array of FormEntryCaptions for current FormIndex.
*
* @return
*/
public FormEntryCaption[] getGroupsForCurrentIndex() {
// return an empty array if you ask for something impossible
if (!(mFormEntryController.getModel().getEvent() == FormEntryController.EVENT_QUESTION
|| mFormEntryController.getModel().getEvent() == FormEntryController.EVENT_PROMPT_NEW_REPEAT || mFormEntryController
.getModel().getEvent() == FormEntryController.EVENT_GROUP)) {
return new FormEntryCaption[0];
}
// the first caption is the question, so we skip it if it's an EVENT_QUESTION
// otherwise, the first caption is a group so we start at index 0
int lastquestion = 1;
if (mFormEntryController.getModel().getEvent() == FormEntryController.EVENT_PROMPT_NEW_REPEAT
|| mFormEntryController.getModel().getEvent() == FormEntryController.EVENT_GROUP) {
lastquestion = 0;
}
FormEntryCaption[] v = mFormEntryController.getModel().getCaptionHierarchy();
FormEntryCaption[] groups = new FormEntryCaption[v.length - lastquestion];
for (int i = 0; i < v.length - lastquestion; i++) {
groups[i] = v[i];
}
return groups;
}
/**
* This is used to enable/disable the "Delete Repeat" menu option.
*
* @return
*/
public boolean indexContainsRepeatableGroup() {
FormEntryCaption[] groups = mFormEntryController.getModel().getCaptionHierarchy();
if (groups.length == 0) {
return false;
}
for (int i = 0; i < groups.length; i++) {
if (groups[i].repeats())
return true;
}
return false;
}
/**
* The count of the closest group that repeats or -1.
*/
public int getLastRepeatedGroupRepeatCount() {
FormEntryCaption[] groups = mFormEntryController.getModel().getCaptionHierarchy();
if (groups.length > 0) {
for (int i = groups.length - 1; i > -1; i--) {
if (groups[i].repeats()) {
return groups[i].getMultiplicity();
}
}
}
return -1;
}
/**
* The name of the closest group that repeats or null.
*/
public String getLastRepeatedGroupName() {
FormEntryCaption[] groups = mFormEntryController.getModel().getCaptionHierarchy();
// no change
if (groups.length > 0) {
for (int i = groups.length - 1; i > -1; i--) {
if (groups[i].repeats()) {
return groups[i].getLongText();
}
}
}
return null;
}
/**
* The closest group the prompt belongs to.
*
* @return FormEntryCaption
*/
private FormEntryCaption getLastGroup() {
FormEntryCaption[] groups = mFormEntryController.getModel().getCaptionHierarchy();
if (groups == null || groups.length == 0)
return null;
else
return groups[groups.length - 1];
}
/**
* The repeat count of closest group the prompt belongs to.
*/
public int getLastRepeatCount() {
if (getLastGroup() != null) {
return getLastGroup().getMultiplicity();
}
return -1;
}
/**
* The text of closest group the prompt belongs to.
*/
public String getLastGroupText() {
if (getLastGroup() != null) {
return getLastGroup().getLongText();
}
return null;
}
}