package org.commcare.activities;
import android.content.DialogInterface;
import android.graphics.Rect;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.view.ViewGroup.LayoutParams;
import android.widget.Toast;
import org.commcare.activities.components.FormEntryConstants;
import org.commcare.activities.components.FormLayoutHelpers;
import org.commcare.activities.components.FormNavigationController;
import org.commcare.activities.components.FormNavigationUI;
import org.commcare.activities.components.FormRelevancyUpdating;
import org.commcare.dalvik.R;
import org.commcare.interfaces.CommCareActivityUIController;
import org.commcare.google.services.analytics.GoogleAnalyticsFields;
import org.commcare.google.services.analytics.GoogleAnalyticsUtils;
import org.commcare.utils.BlockingActionsManager;
import org.commcare.utils.CompoundIntentList;
import org.commcare.utils.StringUtils;
import org.commcare.views.QuestionsView;
import org.commcare.views.UserfacingErrorHandling;
import org.commcare.views.dialogs.DialogChoiceItem;
import org.commcare.views.dialogs.HorizontalPaneledChoiceDialog;
import org.commcare.views.dialogs.PaneledChoiceDialog;
import org.commcare.views.media.AudioController;
import org.commcare.views.widgets.IntentWidget;
import org.commcare.views.widgets.QuestionWidget;
import org.javarosa.core.model.Constants;
import org.javarosa.core.model.FormIndex;
import org.javarosa.core.model.SelectChoice;
import org.javarosa.core.model.data.InvalidData;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.locale.Localization;
import org.javarosa.form.api.FormEntryController;
import org.javarosa.form.api.FormEntryPrompt;
import org.javarosa.xpath.XPathException;
import org.javarosa.xpath.XPathTypeMismatchException;
import org.javarosa.xpath.XPathUnhandledException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.Vector;
public class FormEntryActivityUIController implements CommCareActivityUIController,
Animation.AnimationListener {
private static final String TAG = FormEntryActivityUIController.class.getSimpleName();
private final FormEntryActivity activity;
private ViewGroup mViewPane;
private boolean shouldHideGroupLabel = false;
private Animation mInAnimation;
private Animation mOutAnimation;
// used to limit forward/backward swipes to one per question
private boolean isAnimatingSwipe;
private boolean isDialogShowing;
protected QuestionsView questionsView;
private boolean hasGroupLabel = false;
private int indexOfLastChangedWidget = -1;
private BlockingActionsManager blockingActionsManager;
private boolean formRelevanciesUpdateInProgress = false;
private static final String KEY_LAST_CHANGED_WIDGET = "index-of-last-changed-widget";
enum AnimationType {
LEFT, RIGHT, FADE
}
public FormEntryActivityUIController(FormEntryActivity activity) {
this.activity = activity;
}
@Override
public void setupUI() {
activity.setContentView(R.layout.screen_form_entry);
blockingActionsManager = new BlockingActionsManager(this.activity);
ImageButton nextButton = (ImageButton)activity.findViewById(R.id.nav_btn_next);
ImageButton prevButton = (ImageButton)activity.findViewById(R.id.nav_btn_prev);
Button multiIntentDispatchButton = (Button)activity.findViewById(R.id.multiple_intent_dispatch_button);
View finishButton = activity.findViewById(R.id.nav_btn_finish);
TextView finishText = (TextView)finishButton.findViewById(R.id.nav_btn_finish_text);
finishText.setText(Localization.get("form.entry.finish.button").toUpperCase());
nextButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GoogleAnalyticsUtils.reportFormNavForward(
GoogleAnalyticsFields.LABEL_ARROW,
GoogleAnalyticsFields.VALUE_FORM_NOT_DONE);
showNextView();
}
});
prevButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!FormEntryConstants.NAV_STATE_QUIT.equals(v.getTag())) {
GoogleAnalyticsUtils.reportFormNavBackward(GoogleAnalyticsFields.LABEL_ARROW);
showPreviousView(true);
} else {
GoogleAnalyticsUtils.reportFormQuitAttempt(GoogleAnalyticsFields.LABEL_PROGRESS_BAR_ARROW);
activity.triggerUserQuitInput();
}
}
});
finishButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GoogleAnalyticsUtils.reportFormNavForward(
GoogleAnalyticsFields.LABEL_ARROW,
GoogleAnalyticsFields.VALUE_FORM_DONE);
activity.triggerUserFormComplete();
}
});
multiIntentDispatchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
activity.fireCompoundIntentDispatch();
}
});
mViewPane = (ViewGroup)activity.findViewById(R.id.form_entry_pane);
activity.requestMajorLayoutUpdates();
if (questionsView != null) {
questionsView.teardownView();
}
// re-set defaults in case the app got in a bad state.
isAnimatingSwipe = false;
isDialogShowing = false;
questionsView = null;
mInAnimation = null;
mOutAnimation = null;
}
@Override
public void refreshView() {
refreshCurrentView();
}
/**
* Refreshes the current view. the controller and the displayed view can get out of sync due to
* dialogs and restarts caused by screen orientation changes, so they're resynchronized here.
*/
private void refreshCurrentView() {
refreshCurrentView(true);
}
/**
* Refreshes the current view. the controller and the displayed view can get out of sync due to
* dialogs and restarts caused by screen orientation changes, so they're resynchronized here.
*/
protected void refreshCurrentView(boolean animateLastView) {
if (FormEntryActivity.mFormController == null) {
throw new RuntimeException("Form state is lost! Cannot refresh current view. This shouldn't happen, please submit a bug report.");
}
int event = FormEntryActivity.mFormController.getEvent();
// When we refresh, repeat dialog state isn't maintained, so step back to the previous
// question.
// Also, if we're within a group labeled 'field list', step back to the beginning of that
// group.
// That is, skip backwards over repeat prompts, groups that are not field-lists,
// repeat events, and indexes in field-lists that is not the containing group.
while (event == FormEntryController.EVENT_PROMPT_NEW_REPEAT
|| (event == FormEntryController.EVENT_GROUP && !FormEntryActivity.mFormController.indexIsInFieldList())
|| event == FormEntryController.EVENT_REPEAT
|| (FormEntryActivity.mFormController.indexIsInFieldList() && !(event == FormEntryController.EVENT_GROUP))) {
event = FormEntryActivity.mFormController.stepToPreviousEvent();
}
//If we're at the beginning of form event, but don't show the screen for that, we need
//to get the next valid screen
if (event == FormEntryController.EVENT_BEGINNING_OF_FORM) {
showNextView(true);
} else if (event == FormEntryController.EVENT_END_OF_FORM) {
showPreviousView(false);
} else {
QuestionsView current = createView();
showView(current, AnimationType.FADE, animateLastView);
}
}
/**
* Displays the View specified by the parameter 'next', animating both the current view and next
* appropriately given the AnimationType. Also updates the progress bar.
*/
private void showView(QuestionsView next, AnimationType from) {
showView(next, from, true);
}
private void showView(QuestionsView next, AnimationType from, boolean animateLastView) {
switch (from) {
case RIGHT:
mInAnimation = AnimationUtils.loadAnimation(activity, R.anim.push_left_in);
mOutAnimation = AnimationUtils.loadAnimation(activity, R.anim.push_left_out);
break;
case LEFT:
mInAnimation = AnimationUtils.loadAnimation(activity, R.anim.push_right_in);
mOutAnimation = AnimationUtils.loadAnimation(activity, R.anim.push_right_out);
break;
case FADE:
mInAnimation = AnimationUtils.loadAnimation(activity, R.anim.fade_in);
mOutAnimation = AnimationUtils.loadAnimation(activity, R.anim.fade_out);
break;
}
if (questionsView != null) {
if (animateLastView) {
questionsView.startAnimation(mOutAnimation);
}
mViewPane.removeView(questionsView);
questionsView.teardownView();
}
mInAnimation.setAnimationListener(this);
RelativeLayout.LayoutParams lp =
new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
questionsView = next;
mViewPane.addView(questionsView, lp);
questionsView.startAnimation(mInAnimation);
questionsView.setFocus(activity, indexOfLastChangedWidget);
setupGroupLabel();
}
private void setupGroupLabel() {
hasGroupLabel = false;
FormLayoutHelpers.updateGroupViewVisibility(activity, false, shouldHideGroupLabel);
SpannableStringBuilder groupLabelText = questionsView.getGroupLabel();
if (groupLabelText != null && !groupLabelText.toString().trim().equals("")) {
TextView groupLabel = (TextView)activity.findViewById(R.id.form_entry_group_label);
groupLabel.setText(groupLabelText);
hasGroupLabel = true;
FormLayoutHelpers.updateGroupViewVisibility(activity, true, shouldHideGroupLabel);
}
updateCompoundIntentButtonVisibility();
}
/**
* Determines what should be displayed between a question, or the start screen and displays the
* appropriate view. Also saves answers to the data model without checking constraints.
*/
protected void showPreviousView(boolean showSwipeAnimation) {
if (shouldIgnoreNavigationAction()) {
return;
}
// The answer is saved on a back swipe, but question constraints are ignored.
if (activity.currentPromptIsQuestion()) {
activity.saveAnswersForCurrentScreen(FormEntryConstants.DO_NOT_EVALUATE_CONSTRAINTS);
}
// Any info stored about the last changed widget is useless when we move to a new view
resetLastChangedWidget();
FormIndex startIndex = FormEntryActivity.mFormController.getFormIndex();
FormIndex lastValidIndex = startIndex;
if (FormEntryActivity.mFormController.getEvent() != FormEntryController.EVENT_BEGINNING_OF_FORM) {
int event = FormEntryActivity.mFormController.stepToPreviousEvent();
//Step backwards until we either find a question, the beginning of the form,
//or a field list with valid questions inside
while (event != FormEntryController.EVENT_BEGINNING_OF_FORM
&& event != FormEntryController.EVENT_QUESTION
&& !(event == FormEntryController.EVENT_GROUP
&& FormEntryActivity.mFormController.indexIsInFieldList() && FormEntryActivity.mFormController
.getQuestionPrompts().length != 0)) {
event = FormEntryActivity.mFormController.stepToPreviousEvent();
lastValidIndex = FormEntryActivity.mFormController.getFormIndex();
}
if (event == FormEntryController.EVENT_BEGINNING_OF_FORM) {
// we can't go all the way back to the beginning, so we've
// gotta hit the last index that was valid
FormEntryActivity.mFormController.jumpToIndex(lastValidIndex);
//Did we jump at all? (not sure how we could have, but there might be a mismatch)
if (lastValidIndex.equals(startIndex)) {
//If not, don't even bother changing the view.
//NOTE: This needs to be the same as the
//exit condition below, in case either changes
activity.triggerUserQuitInput();
} else if (lastValidIndex.isBeginningOfFormIndex()) {
//We might have walked all the way back still, which isn't great,
//so keep moving forward again until we find it
//there must be a repeat between where we started and the beginning of hte form, walk back up to it
showNextView(true);
}
} else {
AudioController.INSTANCE.releaseCurrentMediaEntity();
QuestionsView next = createView();
if (showSwipeAnimation) {
showView(next, AnimationType.LEFT);
} else {
showView(next, AnimationType.FADE, false);
}
}
} else {
activity.triggerUserQuitInput();
}
}
private QuestionsView createView() {
activity.setTitle(activity.getHeaderString());
QuestionsView odkv;
// should only be a group here if the event_group is a field-list
try {
odkv =
new QuestionsView(activity, FormEntryActivity.mFormController.getQuestionPrompts(),
FormEntryActivity.mFormController.getGroupsForCurrentIndex(),
FormEntryActivity.mFormController.getWidgetFactory(), activity,
blockingActionsManager);
Log.i(TAG, "created view for group");
} catch (RuntimeException e) {
Logger.exception(e);
UserfacingErrorHandling.createErrorDialog(activity, e.getMessage(), FormEntryConstants.EXIT);
// this is badness to avoid a crash.
// really a next view should increment the formcontroller, create the view
// if the view is null, then keep the current view and pop an error.
return new QuestionsView(activity, blockingActionsManager);
}
// Makes a "clear answer" menu pop up on long-click of
// select-one/select-multiple questions
for (QuestionWidget qw : odkv.getWidgets()) {
if (!qw.getPrompt().isReadOnly() &&
!FormEntryActivity.mFormController.isFormReadOnly() &&
(qw.getPrompt().getControlType() == Constants.CONTROL_SELECT_ONE ||
qw.getPrompt().getControlType() == Constants.CONTROL_SELECT_MULTI)) {
activity.registerForContextMenu(qw);
}
}
FormNavigationUI.updateNavigationCues(activity, FormEntryActivity.mFormController, odkv);
return odkv;
}
/**
* Determines what should be displayed on the screen. Possible options are: a question, an ask
* repeat dialog, or the submit screen. Also saves answers to the data model after checking
* constraints.
*/
protected void showNextView() {
showNextView(false);
}
private void showNextView(boolean resuming) {
AudioController.INSTANCE.releaseCurrentMediaEntity();
if (shouldIgnoreNavigationAction()) {
isAnimatingSwipe = false;
return;
}
if (activity.currentPromptIsQuestion()) {
if (!activity.saveAnswersForCurrentScreen(FormEntryConstants.EVALUATE_CONSTRAINTS)) {
// A constraint was violated so a dialog should be showing.
return;
}
}
// Any info stored about the last changed widget is useless when we move to a new view
resetLastChangedWidget();
if (FormEntryActivity.mFormController.getEvent() != FormEntryController.EVENT_END_OF_FORM) {
int event;
try {
group_skip:
do {
event = FormEntryActivity.mFormController.stepToNextEvent(FormEntryController.STEP_OVER_GROUP);
switch (event) {
case FormEntryController.EVENT_QUESTION:
QuestionsView next = createView();
if (!resuming) {
showView(next, AnimationType.RIGHT);
} else {
showView(next, AnimationType.FADE, false);
}
break group_skip;
case FormEntryController.EVENT_END_OF_FORM:
// auto-advance questions might advance past the last form quesion
activity.triggerUserFormComplete();
break group_skip;
case FormEntryController.EVENT_PROMPT_NEW_REPEAT:
createRepeatDialog();
break group_skip;
case FormEntryController.EVENT_GROUP:
//We only hit this event if we're at the _opening_ of a field
//list, so it seems totally fine to do it this way, technically
//though this should test whether the index is the field list
//host.
if (FormEntryActivity.mFormController.indexIsInFieldList()
&& FormEntryActivity.mFormController.getQuestionPrompts().length != 0) {
QuestionsView nextGroupView = createView();
if (!resuming) {
showView(nextGroupView, AnimationType.RIGHT);
} else {
showView(nextGroupView, AnimationType.FADE, false);
}
break group_skip;
}
// otherwise it's not a field-list group, so just skip it
break;
case FormEntryController.EVENT_REPEAT:
Log.i(TAG, "repeat: " + FormEntryActivity.mFormController.getFormIndex().getReference());
// skip repeats
break;
case FormEntryController.EVENT_REPEAT_JUNCTURE:
Log.i(TAG, "repeat juncture: "
+ FormEntryActivity.mFormController.getFormIndex().getReference());
// skip repeat junctures until we implement them
break;
default:
Log.w(TAG,
"JavaRosa added a new EVENT type and didn't tell us... shame on them.");
break;
}
} while (event != FormEntryController.EVENT_END_OF_FORM);
} catch (XPathTypeMismatchException e) {
UserfacingErrorHandling.logErrorAndShowDialog(activity, e, FormEntryConstants.EXIT);
}
}
}
/**
* Creates and displays a dialog asking the user if they'd like to create a repeat of the
* current group.
*/
private void createRepeatDialog() {
isDialogShowing = true;
// Determine the effect that back and next buttons should have
FormNavigationController.NavigationDetails details;
try {
details = FormNavigationController.calculateNavigationStatus(FormEntryActivity.mFormController, questionsView);
} catch (XPathTypeMismatchException e) {
UserfacingErrorHandling.logErrorAndShowDialog(activity, e, FormEntryConstants.EXIT);
return;
}
final boolean backExitsForm = !details.relevantBeforeCurrentScreen;
final boolean nextExitsForm = details.relevantAfterCurrentScreen == 0;
// Assign title and text strings based on the current state
String backText = Localization.get("repeat.dialog.go.back");
String addAnotherText = Localization.get("repeat.dialog.add");
String title, skipText;
if (!nextExitsForm) {
skipText = Localization.get("repeat.dialog.leave");
} else {
skipText = Localization.get("repeat.dialog.exit");
}
if (FormEntryActivity.mFormController.getLastRepeatCount() > 0) {
title = Localization.get("repeat.dialog.add.another", FormEntryActivity.mFormController.getLastGroupText());
} else {
title = Localization.get("repeat.dialog.add.new", FormEntryActivity.mFormController.getLastGroupText());
}
// Create the choice dialog
ContextThemeWrapper wrapper = new ContextThemeWrapper(activity, R.style.DialogBaseTheme);
final PaneledChoiceDialog dialog = new HorizontalPaneledChoiceDialog(wrapper, title);
// Panel 1: Back option
View.OnClickListener backListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (backExitsForm) {
activity.triggerUserQuitInput();
} else {
dialog.dismiss();
refreshCurrentView(false);
}
}
};
int backIconId;
if (backExitsForm) {
backIconId = R.drawable.icon_exit;
} else {
backIconId = R.drawable.icon_back;
}
DialogChoiceItem backItem = new DialogChoiceItem(backText, backIconId, backListener);
// Panel 2: Add another option
View.OnClickListener addAnotherListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
try {
FormEntryActivity.mFormController.newRepeat();
} catch (XPathUnhandledException | XPathTypeMismatchException e) {
Logger.exception(e);
UserfacingErrorHandling.logErrorAndShowDialog(activity, e, FormEntryConstants.EXIT);
return;
}
showNextView();
}
};
DialogChoiceItem addAnotherItem = new DialogChoiceItem(addAnotherText, R.drawable.icon_new, addAnotherListener);
// Panel 3: Skip option
View.OnClickListener skipListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
if (!nextExitsForm) {
showNextView();
} else {
activity.triggerUserFormComplete();
}
}
};
int skipIconId;
if (nextExitsForm) {
skipIconId = R.drawable.icon_done;
} else {
skipIconId = R.drawable.icon_next;
}
DialogChoiceItem skipItem = new DialogChoiceItem(skipText, skipIconId, skipListener);
dialog.setChoiceItems(new DialogChoiceItem[]{backItem, addAnotherItem, skipItem});
dialog.makeNotCancelable();
dialog.setOnDismissListener(
new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface d) {
isDialogShowing = false;
}
}
);
// Purposefully don't persist this dialog across rotation! Rotation
// refreshes the view, which steps the form index back from the repeat
// event. This can be fixed, but the dialog click listeners closures
// capture refences to the old activity, so we need to redo our
// infrastructure to forward new activities.
dialog.showNonPersistentDialog();
}
protected void next() {
if (!shouldIgnoreSwipeAction()) {
isAnimatingSwipe = true;
showNextView();
}
}
protected boolean shouldIgnoreNavigationAction() {
return blockingActionsManager.isBlocked();
}
protected boolean shouldIgnoreSwipeAction() {
return isAnimatingSwipe || isDialogShowing;
}
/**
* Creates and displays a dialog displaying the violated constraint.
*/
protected void showConstraintWarning(FormIndex index, String constraintText,
int saveStatus, boolean requestFocus) {
switch (saveStatus) {
case FormEntryController.ANSWER_CONSTRAINT_VIOLATED:
if (constraintText == null) {
constraintText = StringUtils.getStringRobust(activity, R.string.invalid_answer_error);
}
break;
case FormEntryController.ANSWER_REQUIRED_BUT_EMPTY:
constraintText = StringUtils.getStringRobust(activity, R.string.required_answer_error);
break;
}
boolean displayed = false;
//We need to see if question in violation is on the screen, so we can show this cleanly.
for (QuestionWidget q : questionsView.getWidgets()) {
if (index.equals(q.getFormId())) {
if (q.getAnswer() instanceof InvalidData) {
constraintText = ((InvalidData)q.getAnswer()).getErrorMessage();
}
q.notifyInvalid(constraintText, requestFocus);
displayed = true;
break;
}
}
if (!displayed) {
Toast popupMessage = Toast.makeText(activity, constraintText, Toast.LENGTH_SHORT);
// center message to avoid overlapping with keyboard
popupMessage.setGravity(Gravity.CENTER, 0, 0);
popupMessage.show();
}
isAnimatingSwipe = false;
}
@Override
public void onAnimationEnd(Animation arg0) {
isAnimatingSwipe = false;
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationStart(Animation animation) {
}
protected void setIsAnimatingSwipe() {
isAnimatingSwipe = true;
}
private boolean hasGroupLabel() {
return hasGroupLabel;
}
protected void recalcShouldHideGroupLabel(Rect newRootViewDimensions) {
shouldHideGroupLabel =
FormLayoutHelpers.determineNumberOfValidGroupLines(activity, newRootViewDimensions,
hasGroupLabel(), shouldHideGroupLabel);
}
protected void restoreFocusToCalloutQuestion() {
int restoredFocusTo =
questionsView.restoreFocusToQuestionThatCalledOut(activity, activity.getPendingWidget());
if (restoredFocusTo != -1) {
indexOfLastChangedWidget = restoredFocusTo;
}
}
protected void saveInstanceState(Bundle outState) {
outState.putInt(KEY_LAST_CHANGED_WIDGET, indexOfLastChangedWidget);
}
protected void restoreSavedState(Bundle savedInstanceState) {
if (savedInstanceState.containsKey(KEY_LAST_CHANGED_WIDGET)) {
indexOfLastChangedWidget = savedInstanceState.getInt(KEY_LAST_CHANGED_WIDGET);
}
}
private void resetLastChangedWidget() {
indexOfLastChangedWidget = -1;
}
protected void recordLastChangedWidgetIndex(QuestionWidget changedWidget) {
indexOfLastChangedWidget = questionsView.getWidgets().indexOf(changedWidget);
}
/**
* Identifies whether the questionlist featues an aggregatable intent callout and
* displays the appropriate button if so.
*/
private void updateCompoundIntentButtonVisibility() {
CompoundIntentList i = questionsView.getAggregateIntentCallout();
if (i == null) {
hideCompoundIntentCalloutButton();
} else {
Button compoundDispatchButton =
(Button)activity.findViewById(R.id.multiple_intent_dispatch_button);
compoundDispatchButton.setVisibility(View.VISIBLE);
compoundDispatchButton.setText(i.getTitle() + ": " + i.getNumberOfCallouts());
}
}
protected void hideCompoundIntentCalloutButton() {
activity.findViewById(R.id.multiple_intent_dispatch_button).setVisibility(View.GONE);
}
protected void updateFormRelevancies() {
if (formRelevanciesUpdateInProgress) {
// Don't allow this method to call itself downstream accidentally
return;
}
formRelevanciesUpdateInProgress = true;
ArrayList<QuestionWidget> oldWidgets = questionsView.getWidgets();
// These 2 calls need to be made here, rather than in the for loop below, because at that
// point the widgets will have already started being updated to the values for the new view
ArrayList<Vector<SelectChoice>> oldSelectChoices =
FormRelevancyUpdating.getOldSelectChoicesForEachWidget(oldWidgets);
ArrayList<String> oldQuestionTexts =
FormRelevancyUpdating.getOldQuestionTextsForEachWidget(oldWidgets);
activity.saveAnswersForCurrentScreen(FormEntryConstants.DO_NOT_EVALUATE_CONSTRAINTS);
FormEntryPrompt[] newValidPrompts;
try {
newValidPrompts = FormEntryActivity.mFormController.getQuestionPrompts();
} catch (XPathException e) {
UserfacingErrorHandling.logErrorAndShowDialog(activity, e, FormEntryConstants.EXIT);
return;
}
Set<FormEntryPrompt> promptsLeftInView = new HashSet<>();
ArrayList<Integer> shouldRemoveFromView = new ArrayList<>();
// Loop through all of the old widgets to determine which ones should stay in the new view
for (int i = 0; i < oldWidgets.size(); i++) {
//Intent widgets need to be fully rebuilt to update their intent callouts
//depending on model changes.
if (oldWidgets.get(i) instanceof IntentWidget) {
shouldRemoveFromView.add(i);
continue;
}
FormEntryPrompt oldPrompt = oldWidgets.get(i).getPrompt();
String priorQuestionTextForThisWidget = oldQuestionTexts.get(i);
Vector<SelectChoice> priorSelectChoicesForThisWidget = oldSelectChoices.get(i);
FormEntryPrompt equivalentNewPrompt =
FormRelevancyUpdating.getEquivalentPromptInNewList(newValidPrompts,
oldPrompt, priorQuestionTextForThisWidget, priorSelectChoicesForThisWidget);
if (equivalentNewPrompt != null) {
promptsLeftInView.add(equivalentNewPrompt);
} else {
// If there is no equivalent prompt in the list of new prompts, then this prompt is
// no longer relevant in the new view, so it should get removed
shouldRemoveFromView.add(i);
}
}
// Remove "atomically" to not mess up iterations
questionsView.removeQuestionsFromIndex(shouldRemoveFromView);
// Now go through add add any new prompts that we need
for (int i = 0; i < newValidPrompts.length; ++i) {
FormEntryPrompt prompt = newValidPrompts[i];
if (!promptsLeftInView.contains(prompt)) {
// If the old version of this prompt was NOT left in the view, then add it
questionsView.addQuestionToIndex(prompt, FormEntryActivity.mFormController.getWidgetFactory(), i);
}
}
updateCompoundIntentButtonVisibility();
formRelevanciesUpdateInProgress = false;
}
}