<commit_before>package org.odk.collect.android.activities;

import android.annotation.SuppressLint;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.location.LocationManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.provider.MediaStore.Images;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.text.SpannableStringBuilder;
import android.util.Log;
import android.util.Pair;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.ContextThemeWrapper;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;

import org.commcare.android.framework.CommCareActivity;
import org.commcare.android.javarosa.AndroidLogger;
import org.commcare.android.logic.BarcodeScanListenerDefaultImpl;
import org.commcare.android.util.FormUploadUtil;
import org.commcare.android.util.SessionUnavailableException;
import org.commcare.android.util.StringUtils;
import org.commcare.dalvik.R;
import org.commcare.dalvik.activities.CommCareHomeActivity;
import org.commcare.dalvik.application.CommCareApplication;
import org.commcare.dalvik.dialogs.AlertDialogFactory;
import org.commcare.dalvik.dialogs.CustomProgressDialog;
import org.commcare.dalvik.odk.provider.FormsProviderAPI.FormsColumns;
import org.commcare.dalvik.odk.provider.InstanceProviderAPI;
import org.commcare.dalvik.odk.provider.InstanceProviderAPI.InstanceColumns;
import org.commcare.dalvik.utils.UriToFilePath;
import org.javarosa.core.model.Constants;
import org.javarosa.core.model.FormIndex;
import org.javarosa.core.model.data.IAnswerData;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.locale.Localization;
import org.javarosa.core.services.locale.Localizer;
import org.javarosa.form.api.FormEntryController;
import org.javarosa.form.api.FormEntryPrompt;
import org.javarosa.model.xform.XFormsModule;
import org.javarosa.xpath.XPathException;
import org.javarosa.xpath.XPathTypeMismatchException;
import org.odk.collect.android.activities.components.FormNavigationController;
import org.odk.collect.android.activities.components.FormNavigationUI;
import org.odk.collect.android.activities.components.ImageCaptureProcessing;
import org.odk.collect.android.application.ODKStorage;
import org.odk.collect.android.jr.extensions.IntentCallout;
import org.odk.collect.android.jr.extensions.PollSensorAction;
import org.odk.collect.android.listeners.AdvanceToNextListener;
import org.odk.collect.android.listeners.FormSaveCallback;
import org.odk.collect.android.listeners.FormSavedListener;
import org.odk.collect.android.listeners.WidgetChangedListener;
import org.odk.collect.android.logic.FormController;
import org.odk.collect.android.logic.PropertyManager;
import org.odk.collect.android.preferences.FormEntryPreferences;
import org.odk.collect.android.tasks.FormLoaderTask;
import org.odk.collect.android.tasks.SaveToDiskTask;
import org.odk.collect.android.utilities.Base64Wrapper;
import org.odk.collect.android.utilities.FileUtils;
import org.odk.collect.android.utilities.GeoUtils;
import org.odk.collect.android.views.ODKView;
import org.odk.collect.android.views.ResizingImageView;
import org.odk.collect.android.widgets.DateTimeWidget;
import org.odk.collect.android.widgets.ImageWidget;
import org.odk.collect.android.widgets.IntentWidget;
import org.odk.collect.android.widgets.QuestionWidget;
import org.odk.collect.android.widgets.TimeWidget;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.crypto.spec.SecretKeySpec;

/**
 * Displays questions, animates transitions between
 * questions, and allows the user to enter data.
 * 
 * @author <NAME> (<EMAIL>)
 */
public class FormEntryActivity extends CommCareActivity<FormEntryActivity>
        implements AnimationListener, FormSavedListener, FormSaveCallback,
        AdvanceToNextListener, WidgetChangedListener {
    private static final String TAG = FormEntryActivity.class.getSimpleName();

    // Defines for FormEntryActivity
    private static final boolean EXIT = true;
    private static final boolean DO_NOT_EXIT = false;
    private static final boolean EVALUATE_CONSTRAINTS = true;
    private static final boolean DO_NOT_EVALUATE_CONSTRAINTS = false;

    // Request codes for returning data from specified intent.
    public static final int IMAGE_CAPTURE = 1;
    public static final int BARCODE_CAPTURE = 2;
    public static final int AUDIO_VIDEO_FETCH = 3;
    public static final int LOCATION_CAPTURE = 5;
    private static final int HIERARCHY_ACTIVITY = 6;
    public static final int IMAGE_CHOOSER = 7;
    private static final int FORM_PREFERENCES_KEY = 8;
    public static final int INTENT_CALLOUT = 10;
    private static final int HIERARCHY_ACTIVITY_FIRST_START = 11;
    public static final int SIGNATURE_CAPTURE = 12;

    // Extra returned from gp activity
    public static final String LOCATION_RESULT = "LOCATION_RESULT";

    // Identifies the gp of the form used to launch form entry
    private static final String KEY_FORMPATH = "formpath";
    public static final String KEY_INSTANCEDESTINATION = "instancedestination";
    public static final String TITLE_FRAGMENT_TAG = "odk_title_fragment";
    public static final String KEY_FORM_CONTENT_URI = "form_content_uri";
    public static final String KEY_INSTANCE_CONTENT_URI = "instance_content_uri";
    public static final String KEY_AES_STORAGE_KEY = "key_aes_storage";
    public static final String KEY_HEADER_STRING = "form_header";
    public static final String KEY_INCOMPLETE_ENABLED = "org.odk.collect.form.management";
    public static final String KEY_RESIZING_ENABLED = "org.odk.collect.resizing.enabled";
    private static final String KEY_HAS_SAVED = "org.odk.collect.form.has.saved";

    /**
     * Intent extra flag to track if this form is an archive. Used to trigger
     * return logic when this activity exits to the home screen, such as
     * whether to redirect to archive view or sync the form.
     */
    public static final String IS_ARCHIVED_FORM = "is-archive-form";

    // Identifies whether this is a new form, or reloading a form after a screen
    // rotation (or similar)
    private static final String KEY_FORM_LOAD_HAS_TRIGGERED = "newform";
    private static final String KEY_FORM_LOAD_FAILED = "form-failed";

    private static final int MENU_LANGUAGES = Menu.FIRST;
    private static final int MENU_HIERARCHY_VIEW = Menu.FIRST + 1;
    private static final int MENU_SAVE = Menu.FIRST + 2;
    private static final int MENU_PREFERENCES = Menu.FIRST + 3;

    public static final String NAV_STATE_NEXT = "next";

    private String mFormPath;
    // Path to a particular form instance
    public static String mInstancePath;
    private String mInstanceDestination;
    private GestureDetector mGestureDetector;
    
    private SecretKeySpec symetricKey = null;

    public static FormController mFormController;

    private Animation mInAnimation;
    private Animation mOutAnimation;

    private ViewGroup mViewPane;
    private View mCurrentView;

    private boolean mIncompleteEnabled = true;
    private boolean hasFormLoadBeenTriggered = false;
    private boolean hasFormLoadFailed = false;

    // used to limit forward/backward swipes to one per question
    private boolean mBeenSwiped;

    private FormLoaderTask<FormEntryActivity> mFormLoaderTask;
    private SaveToDiskTask<FormEntryActivity> mSaveToDiskTask;
    
    private Uri formProviderContentURI = FormsColumns.CONTENT_URI;
    private Uri instanceProviderContentURI = InstanceColumns.CONTENT_URI;
    
    private static String mHeaderString;
    
    // Was the form saved? Used to set activity return code.
    private boolean hasSaved = false;

    private BroadcastReceiver mLocationServiceIssueReceiver;

    // marked true if we are in the process of saving a form because the user
    // database & key session are expiring. Being set causes savingComplete to
    // broadcast a form saving intent.
    private boolean savingFormOnKeySessionExpiration = false;

    enum AnimationType {
        LEFT, RIGHT, FADE
    }

    @Override
    @SuppressLint("NewApi")
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        addBreadcrumbBar();

        // must be at the beginning of any activity that can be called from an external intent
        try {
            ODKStorage.createODKDirs();
        } catch (RuntimeException e) {
            Logger.exception(e);
            CommCareActivity.createErrorDialog(this, e.getMessage(), EXIT);
            return;
        }

        setupUI();

        // Load JavaRosa modules. needed to restore forms.
        new XFormsModule().registerModule();

        // needed to override rms property manager
        org.javarosa.core.services.PropertyManager.setPropertyManager(new PropertyManager(
                getApplicationContext()));

        loadStateFromBundle(savedInstanceState);

        // Check to see if this is a screen flip or a new form load.
        Object data = this.getLastCustomNonConfigurationInstance();
        if (data instanceof FormLoaderTask) {
            mFormLoaderTask = (FormLoaderTask) data;
        } else if (data instanceof SaveToDiskTask) {
            mSaveToDiskTask = (SaveToDiskTask) data;
            mSaveToDiskTask.setFormSavedListener(this);
        } else if (hasFormLoadBeenTriggered && !hasFormLoadFailed) {
            // Screen orientation change
            refreshCurrentView();
        }
    }

    @Override
    public boolean onMenuItemSelected(int featureId, MenuItem item) {
        /*
         * EventLog accepts only proper Strings as input, but prior to this version,
         * Android would try to send SpannedStrings to it, thus crashing the app.
         * This makes sure the title is actually a String.
         * This fixes bug 174626.
         */
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2
                && item.getTitleCondensed() != null) {
            item.setTitleCondensed(item.getTitleCondensed().toString());
        }

        return super.onMenuItemSelected(featureId, item);
    }

    @Override
    public void formSaveCallback() {
        // note that we have started saving the form
        savingFormOnKeySessionExpiration = true;
        // start saving form, which will call the key session logout completion
        // function when it finishes.
        saveIncompleteFormToDisk(EXIT, null, true);
    }

    private void registerFormEntryReceiver() {
        //BroadcastReceiver for:
        // a) An unresolvable xpath expression encountered in PollSensorAction.onLocationChanged
        // b) Checking if GPS services are not available
        mLocationServiceIssueReceiver = new BroadcastReceiver() {

            @Override
            public void onReceive(Context context, Intent intent) {
                context.removeStickyBroadcast(intent);
                String action = intent.getAction();
                if (GeoUtils.ACTION_CHECK_GPS_ENABLED.equals(action)) {
                    handleNoGpsBroadcast(context);
                } else if (PollSensorAction.XPATH_ERROR_ACTION.equals(action)) {
                    handleXpathErrorBroadcast(intent);
                }
            }
        };

        IntentFilter filter = new IntentFilter();
        filter.addAction(PollSensorAction.XPATH_ERROR_ACTION);
        filter.addAction(GeoUtils.ACTION_CHECK_GPS_ENABLED);
        registerReceiver(mLocationServiceIssueReceiver, filter);
    }

    private void handleNoGpsBroadcast(Context context) {
        LocationManager manager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
        Set<String> providers = GeoUtils.evaluateProviders(manager);
        if (providers.isEmpty()) {
            DialogInterface.OnClickListener onChangeListener = new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int i) {
                    if (i == DialogInterface.BUTTON_POSITIVE) {
                        Intent intent = new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
                        startActivity(intent);
                    }
                }
            };
            GeoUtils.showNoGpsDialog(this, onChangeListener);
        }
    }

    private void handleXpathErrorBroadcast(Intent intent) {
        String problemXpath = intent.getStringExtra(PollSensorAction.KEY_UNRESOLVED_XPATH);
        CommCareActivity.createErrorDialog(FormEntryActivity.this,
                "There is a bug in one of your form's XPath Expressions \n" + problemXpath, EXIT);
    }

    private void setupUI() {
        setContentView(R.layout.screen_form_entry);

        ImageButton nextButton = (ImageButton)this.findViewById(R.id.nav_btn_next);
        ImageButton prevButton = (ImageButton)this.findViewById(R.id.nav_btn_prev);

        nextButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!"done".equals(v.getTag())) {
                    FormEntryActivity.this.showNextView();
                } else {
                    triggerUserFormComplete();
                }
            }
        });

        prevButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!"quit".equals(v.getTag())) {
                    FormEntryActivity.this.showPreviousView(true);
                } else {
                    FormEntryActivity.this.triggerUserQuitInput();
                }
            }
        });

        mViewPane = (ViewGroup)findViewById(R.id.form_entry_pane);

        mBeenSwiped = false;
        mCurrentView = null;
        mInAnimation = null;
        mOutAnimation = null;
        mGestureDetector = new GestureDetector(this);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString(KEY_FORMPATH, mFormPath);
        outState.putBoolean(KEY_FORM_LOAD_HAS_TRIGGERED, hasFormLoadBeenTriggered);
        outState.putBoolean(KEY_FORM_LOAD_FAILED, hasFormLoadFailed);
        outState.putString(KEY_FORM_CONTENT_URI, formProviderContentURI.toString());
        outState.putString(KEY_INSTANCE_CONTENT_URI, instanceProviderContentURI.toString());
        outState.putString(KEY_INSTANCEDESTINATION, mInstanceDestination);
        outState.putBoolean(KEY_INCOMPLETE_ENABLED, mIncompleteEnabled);
        outState.putBoolean(KEY_HAS_SAVED, hasSaved);
        outState.putString(KEY_RESIZING_ENABLED, ResizingImageView.resizeMethod);
        
        if(symetricKey != null) {
            try {
                outState.putString(KEY_AES_STORAGE_KEY, new Base64Wrapper().encodeToString(symetricKey.getEncoded()));
            } catch (ClassNotFoundException e) {
                // we can't really get here anyway, since we couldn't have decoded the string to begin with
                throw new RuntimeException("Base 64 encoding unavailable! Can't pass storage key");
            }
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
        super.onActivityResult(requestCode, resultCode, intent);

        if (requestCode == FORM_PREFERENCES_KEY) {
            refreshCurrentView(false);
            return;
        }

        if (resultCode == RESULT_CANCELED) {
            if (requestCode == HIERARCHY_ACTIVITY_FIRST_START) {
                // They pressed 'back' on the first hierarchy screen, so we should assume they want
                // to back out of form entry all together
                finishReturnInstance(false);
            } else if (requestCode == INTENT_CALLOUT){
                processIntentResponse(intent, true);
            }
            // request was canceled, so do nothing
            return;
        }

        switch (requestCode) {
            case BARCODE_CAPTURE:
                String sb = intent.getStringExtra(BarcodeScanListenerDefaultImpl.SCAN_RESULT);
                ((ODKView) mCurrentView).setBinaryData(sb);
                saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
                break;
            case INTENT_CALLOUT:
                processIntentResponse(intent);
                break;
            case IMAGE_CAPTURE:
                processCaptureResponse(true);
                break;
            case SIGNATURE_CAPTURE:
                processCaptureResponse(false);
                break;
            case IMAGE_CHOOSER:
                processImageChooserResponse(intent);
                break;
            case AUDIO_VIDEO_FETCH:
                processChooserResponse(intent);
                break;
            case LOCATION_CAPTURE:
                String sl = intent.getStringExtra(LOCATION_RESULT);
                ((ODKView) mCurrentView).setBinaryData(sl);
                saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
                break;
            case HIERARCHY_ACTIVITY:
            case HIERARCHY_ACTIVITY_FIRST_START:
                // We may have jumped to a new index in hierarchy activity, so refresh
                refreshCurrentView(false);
                break;
        }
    }

    /**
     * Processes the return from an image capture intent, launched by either an ImageWidget or
     * SignatureWidget
     *
     * @param isImage true if this was from an ImageWidget, false if it was a SignatureWidget
     */
    private void processCaptureResponse(boolean isImage) {
        /* We saved the image to the tempfile_path, but we really want it to be in:
         * /sdcard/odk/instances/[current instance]/something.[jpg/png/etc] so we move it there
         * before inserting it into the content provider. Once the android image capture bug gets
         * fixed, (read, we move on from Android 1.6) we want to handle images the audio and
         * video
         */

        // The intent is empty, but we know we saved the image to the temp file
        File originalImage = ImageWidget.TEMP_FILE_FOR_IMAGE_CAPTURE;
        try {
            File unscaledFinalImage = ImageCaptureProcessing.moveAndScaleImage(originalImage, isImage, getInstanceFolder(), this);
            saveImageWidgetAnswer(unscaledFinalImage);
        } catch (IOException e) {
            e.printStackTrace();
            showCustomToast(Localization.get("image.capture.not.saved"), Toast.LENGTH_LONG);
        }
    }

    private String getInstanceFolder() {
       return mInstancePath.substring(0, mInstancePath.lastIndexOf("/") + 1);
    }

    private void processImageChooserResponse(Intent intent) {
        /* We have a saved image somewhere, but we really want it to be in:
         * /sdcard/odk/instances/[current instance]/something.[jpg/png/etc] so we move it there
         * before inserting it into the content provider. Once the android image capture bug gets
         * fixed, (read, we move on from Android 1.6) we want to handle images the audio and
         * video
         */

        // get gp of chosen file
        Uri selectedImage = intent.getData();
        File originalImage = new File(FileUtils.getPath(this, selectedImage));

        if (originalImage.exists()) {
            try {
                File unscaledFinalImage = ImageCaptureProcessing.moveAndScaleImage(originalImage, true, getInstanceFolder(), this);
                saveImageWidgetAnswer(unscaledFinalImage);
            } catch (IOException e) {
                e.printStackTrace();
                showCustomToast(Localization.get("image.selection.not.saved"), Toast.LENGTH_LONG);
            }
        } else {
            // The user has managed to select a file from the image browser that doesn't actually
            // exist on the file system anymore
            showCustomToast(Localization.get("invalid.image.selection"), Toast.LENGTH_LONG);
        }
    }

    private void saveImageWidgetAnswer(File unscaledFinalImage) {
        // Add the new image to the Media content provider so that the viewing is fast in Android 2.0+
        ContentValues values = new ContentValues(6);
        values.put(Images.Media.TITLE, unscaledFinalImage.getName());
        values.put(Images.Media.DISPLAY_NAME, unscaledFinalImage.getName());
        values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis());
        values.put(Images.Media.MIME_TYPE, "image/jpeg");
        values.put(Images.Media.DATA, unscaledFinalImage.getAbsolutePath());

        Uri imageURI =
                getContentResolver().insert(Images.Media.EXTERNAL_CONTENT_URI, values);
        Log.i(TAG, "Inserting image returned uri = " + imageURI.toString());

        ((ODKView) mCurrentView).setBinaryData(imageURI);
        saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
        refreshCurrentView();
    }

    private void processChooserResponse(Intent intent) {
        // For audio/video capture/chooser, we get the URI from the content provider
        // then the widget copies the file and makes a new entry in the content provider.
        Uri media = intent.getData();
        String binaryPath = UriToFilePath.getPathFromUri(CommCareApplication._(), media);
        if (!FormUploadUtil.isSupportedMultimediaFile(binaryPath)) {
            // don't let the user select a file that won't be included in the
            // upload to the server
            ((ODKView) mCurrentView).clearAnswer();
            Toast.makeText(FormEntryActivity.this,
                    Localization.get("form.attachment.invalid"),
                    Toast.LENGTH_LONG).show();
        } else {
            ((ODKView) mCurrentView).setBinaryData(media);
        }
        saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
        refreshCurrentView();
    }

    /**
     * Search the the current view's widgets for one that has registered a
     * pending callout with the form controller
     */
    public QuestionWidget getPendingWidget() {
        FormIndex pendingIndex = mFormController.getPendingCalloutFormIndex();
        if (pendingIndex == null) {
            Logger.log(AndroidLogger.SOFT_ASSERT,
                    "getPendingWidget called when pending callout form index was null");
            return null;
        }
        for (QuestionWidget q : ((ODKView)mCurrentView).getWidgets()) {
            if (q.getFormId().equals(pendingIndex)) {
                return q;
            }
        }
        Logger.log(AndroidLogger.SOFT_ASSERT,
                "getPendingWidget couldn't find question widget with a form index that matches the pending callout.");
        return null;
    }

    private void processIntentResponse(Intent response){
        processIntentResponse(response, false);
    }

    private void processIntentResponse(Intent response, boolean cancelled) {
        // keep track of whether we should auto advance
        boolean advance = false;
        boolean quick = false;

        IntentWidget pendingIntentWidget = (IntentWidget)getPendingWidget();
        TreeReference context;
        if (mFormController.getPendingCalloutFormIndex() != null) {
            context = mFormController.getPendingCalloutFormIndex().getReference();
        } else {
            context = null;
        }
        if(pendingIntentWidget != null) {
            //Set our instance destination for binary data if needed
            String destination = mInstancePath.substring(0, mInstancePath.lastIndexOf("/") + 1);
            
            //get the original intent callout
            IntentCallout ic = pendingIntentWidget.getIntentCallout();
            
            quick = "quick".equals(ic.getAppearance());

            //And process it 
            advance = ic.processResponse(response, context, new File(destination));
            
            ic.setCancelled(cancelled);
        }

        refreshCurrentView();

        // auto advance if we got a good result and are in quick mode
        if(advance && quick){
            showNextView();
        }
    }

    private void updateFormRelevencies(){
        saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
        
        if(!(mCurrentView instanceof ODKView)){
            throw new RuntimeException("Tried to update form relevency not on compound view");
        }
        
        ODKView oldODKV = (ODKView)mCurrentView;
        
        FormEntryPrompt[] newValidPrompts = mFormController.getQuestionPrompts();
        Set<FormEntryPrompt> used = new HashSet<>();
        
        ArrayList<QuestionWidget> oldWidgets = oldODKV.getWidgets();

        ArrayList<Integer> removeList = new ArrayList<>();

           for(int i=0;i<oldWidgets.size();i++){
            QuestionWidget oldWidget = oldWidgets.get(i);
            boolean stillRelevent = false;

            for(FormEntryPrompt prompt : newValidPrompts) {
            	if(prompt.getIndex().equals(oldWidget.getPrompt().getIndex())) {
            		stillRelevent = true;
            		used.add(prompt);
            	}
            }
            if(!stillRelevent){
                removeList.add(i);
            }
        }
        // remove "atomically" to not mess up iterations
        oldODKV.removeQuestionsFromIndex(removeList);

        //Now go through add add any new prompts that we need
        for(int i = 0 ; i < newValidPrompts.length; ++i) {
        	FormEntryPrompt prompt = newValidPrompts[i]; 
        	if(used.contains(prompt)) {
        		continue;
        	} 
        	oldODKV.addQuestionToIndex(prompt, mFormController.getWidgetFactory(), i);
        }
    }

	/**
     * 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.
     */
    private void refreshCurrentView(boolean animateLastView) {
        if(mFormController == null) { throw new RuntimeException("Form state is lost! Cannot refresh current view. This shouldn't happen, please submit a bug report."); }
        int event = 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 && !mFormController.indexIsInFieldList())
                || event == FormEntryController.EVENT_REPEAT
                || (mFormController.indexIsInFieldList() && !(event == FormEntryController.EVENT_GROUP))) {
            event = 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 {
            View current = createView();
            showView(current, AnimationType.FADE, animateLastView);
        }
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        menu.removeItem(MENU_LANGUAGES);
        menu.removeItem(MENU_HIERARCHY_VIEW);
        menu.removeItem(MENU_SAVE);
        menu.removeItem(MENU_PREFERENCES);

        if(mIncompleteEnabled) {
            menu.add(0, MENU_SAVE, 0, StringUtils.getStringRobust(this, R.string.save_all_answers)).setIcon(
                    android.R.drawable.ic_menu_save);
        }
        menu.add(0, MENU_HIERARCHY_VIEW, 0, StringUtils.getStringRobust(this, R.string.view_hierarchy)).setIcon(
                R.drawable.ic_menu_goto);

        boolean hasMultipleLanguages =
                (!(mFormController == null || mFormController.getLanguages() == null || mFormController.getLanguages().length == 1));
        menu.add(0, MENU_LANGUAGES, 0, StringUtils.getStringRobust(this, R.string.change_language))
                .setIcon(R.drawable.ic_menu_start_conversation)
                .setEnabled(hasMultipleLanguages);

        
        menu.add(0, MENU_PREFERENCES, 0, StringUtils.getStringRobust(this, R.string.form_entry_settings)).setIcon(
                android.R.drawable.ic_menu_preferences);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case MENU_LANGUAGES:
                createLanguageDialog();
                return true;
            case MENU_SAVE:
                saveFormToDisk(DO_NOT_EXIT, null, false);
                return true;
            case MENU_HIERARCHY_VIEW:
                if (currentPromptIsQuestion()) {
                    saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
                }
                Intent i = new Intent(this, FormHierarchyActivity.class);
                startActivityForResult(i, HIERARCHY_ACTIVITY);
                return true;
            case MENU_PREFERENCES:
                Intent pref = new Intent(this, FormEntryPreferences.class);
                startActivityForResult(pref, FORM_PREFERENCES_KEY);
                return true;
            case android.R.id.home:
                triggerUserQuitInput();
                return true;

        }
        return super.onOptionsItemSelected(item);
    }

    /**
     * @return true If the current index of the form controller contains questions
     */
    private boolean currentPromptIsQuestion() {
        return (mFormController.getEvent() == FormEntryController.EVENT_QUESTION || mFormController
                .getEvent() == FormEntryController.EVENT_GROUP);
    }

    private boolean saveAnswersForCurrentScreen(boolean evaluateConstraints) {
        return saveAnswersForCurrentScreen(evaluateConstraints, true, false);
    }

    /**
     * Attempt to save the answer(s) in the current screen to into the data model.
     *
     * @param failOnRequired      Whether or not the constraint evaluation
     *                            should return false if the question is only
     *                            required. (this is helpful for incomplete
     *                            saves)
     * @param headless            running in a process that can't display graphics
     * @return false if any error occurs while saving (constraint violated,
     * etc...), true otherwise.
     */
    private boolean saveAnswersForCurrentScreen(boolean evaluateConstraints,
                                                boolean failOnRequired,
                                                boolean headless) {
        // only try to save if the current event is a question or a field-list
        // group
        boolean success = true;
        if ((mFormController.getEvent() == FormEntryController.EVENT_QUESTION)
                || ((mFormController.getEvent() == FormEntryController.EVENT_GROUP) &&
                mFormController.indexIsInFieldList())) {
            if (mCurrentView instanceof ODKView) {
                HashMap<FormIndex, IAnswerData> answers =
                        ((ODKView)mCurrentView).getAnswers();

                // Sort the answers so if there are multiple errors, we can
                // bring focus to the first one
                List<FormIndex> indexKeys = new ArrayList<>();
                indexKeys.addAll(answers.keySet());
                Collections.sort(indexKeys, new Comparator<FormIndex>() {
                    @Override
                    public int compare(FormIndex arg0, FormIndex arg1) {
                        return arg0.compareTo(arg1);
                    }
                });

                for (FormIndex index : indexKeys) {
                    // Within a group, you can only save for question events
                    if (mFormController.getEvent(index) == FormEntryController.EVENT_QUESTION) {
                        int saveStatus = saveAnswer(answers.get(index),
                                index, evaluateConstraints);
                        if (evaluateConstraints &&
                                ((saveStatus != FormEntryController.ANSWER_OK) &&
                                        (failOnRequired ||
                                                saveStatus != FormEntryController.ANSWER_REQUIRED_BUT_EMPTY))) {
                            if (!headless) {
                                createConstraintToast(index, mFormController.getQuestionPrompt(index).getConstraintText(), saveStatus, success);
                            }
                            success = false;
                        }
                    } else {
                        Log.w(TAG,
                                "Attempted to save an index referencing something other than a question: "
                                        + index.getReference());
                    }
                }
            } else {
                String viewType;
                if (mCurrentView == null || mCurrentView.getClass() == null) {
                   viewType = "null";
                } else {
                    viewType = mCurrentView.getClass().toString();
                }
                Log.w(TAG, "Unknown view type rendered while current event was question or group! View type: " + viewType);
            }
        }
        return success;
    }

    /**
     * Clears the answer on the screen.
     */
    private void clearAnswer(QuestionWidget qw) {
        qw.clearAnswer();
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);
        menu.add(0, v.getId(), 0, StringUtils.getStringSpannableRobust(this, R.string.clear_answer));
        menu.setHeaderTitle(StringUtils.getStringSpannableRobust(this, R.string.edit_prompt));
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        // We don't have the right view here, so we store the View's ID as the
        // item ID and loop through the possible views to find the one the user
        // clicked on.
        for (QuestionWidget qw : ((ODKView) mCurrentView).getWidgets()) {
            if (item.getItemId() == qw.getId()) {
                createClearDialog(qw);
            }
        }

        return super.onContextItemSelected(item);
    }

    /**
     * If we're loading, then we pass the loading thread to our next instance.
     */
    @Override
    public Object onRetainCustomNonConfigurationInstance() {
        // if a form is loading, pass the loader task
        if (mFormLoaderTask != null && mFormLoaderTask.getStatus() != AsyncTask.Status.FINISHED)
            return mFormLoaderTask;

        // if a form is writing to disk, pass the save to disk task
        if (mSaveToDiskTask != null && mSaveToDiskTask.getStatus() != AsyncTask.Status.FINISHED)
            return mSaveToDiskTask;

        // mFormEntryController is static so we don't need to pass it.
        if (mFormController != null && currentPromptIsQuestion()) {
            saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
        }
        return null;
    }
    
    private String getHeaderString() {
        if(mHeaderString != null) {
            //Localization?
            return mHeaderString;
        } else {
            return StringUtils.getStringRobust(this, R.string.application_name) + " > " + mFormController.getFormTitle();
        }
    }

    private View createView() {
        setTitle(getHeaderString());
        ODKView odkv;
        // should only be a group here if the event_group is a field-list
        try {
            odkv =
                    new ODKView(this, mFormController.getQuestionPrompts(),
                            mFormController.getGroupsForCurrentIndex(),
                            mFormController.getWidgetFactory(), this);
            Log.i(TAG, "created view for group");
        } catch (RuntimeException e) {
            Logger.exception(e);
            CommCareActivity.createErrorDialog(this, e.getMessage(), 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 View(this);
        }

        // 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() &&
                    !mFormController.isFormReadOnly() &&
                    (qw.getPrompt().getControlType() == Constants.CONTROL_SELECT_ONE ||
                            qw.getPrompt().getControlType() == Constants.CONTROL_SELECT_MULTI)) {
                registerForContextMenu(qw);
            }
        }

        FormNavigationUI formNavUi = new FormNavigationUI(this, mCurrentView, mFormController);
        formNavUi.updateNavigationCues(odkv);

        return odkv;
    }

    @SuppressLint("NewApi")
    @Override
    public boolean dispatchTouchEvent(MotionEvent mv) {
        //We need to ignore this even if it's processed by the action
        //bar (if one exists)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            ActionBar bar = getActionBar();
            if (bar != null) {
                View customView = bar.getCustomView();
                if (customView != null && customView.dispatchTouchEvent(mv)) {
                    return true;
                }
            }
        }

        boolean handled = mGestureDetector.onTouchEvent(mv);
        return handled || super.dispatchTouchEvent(mv);
    }

    /**
     * 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.
     */
    private void showNextView() { showNextView(false); }
    private void showNextView(boolean resuming) {
        if (currentPromptIsQuestion()) {
            if (!saveAnswersForCurrentScreen(EVALUATE_CONSTRAINTS)) {
                // A constraint was violated so a dialog should be showing.
                return;
            }
        }

        if (mFormController.getEvent() != FormEntryController.EVENT_END_OF_FORM) {
            int event;
            
            try{
            group_skip: do {
                event = mFormController.stepToNextEvent(FormController.STEP_OVER_GROUP);
                switch (event) {
                    case FormEntryController.EVENT_QUESTION:
                        View next = createView();
                        if (!resuming) {
                            showView(next, AnimationType.RIGHT);
                        } else {
                            showView(next, AnimationType.FADE, false);
                        }
                        break group_skip;
                    case FormEntryController.EVENT_END_OF_FORM:
                        Logger.log(AndroidLogger.SOFT_ASSERT,
                                "Trying to show an end of form event");
                        showPreviousView(false);
                        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 (mFormController.indexIsInFieldList()
                                && mFormController.getQuestionPrompts().length != 0) {
                            View nextGroupView = createView();
                            if(!resuming) {
                                showView(nextGroupView, AnimationType.RIGHT);<commit_msg>Prevent infinite loop if form just has 1 empty group<commit_after>package org.odk.collect.android.activities;

import android.annotation.SuppressLint;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.location.LocationManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.provider.MediaStore.Images;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.text.SpannableStringBuilder;
import android.util.Log;
import android.util.Pair;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.ContextThemeWrapper;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;

import org.commcare.android.framework.CommCareActivity;
import org.commcare.android.javarosa.AndroidLogger;
import org.commcare.android.logic.BarcodeScanListenerDefaultImpl;
import org.commcare.android.util.FormUploadUtil;
import org.commcare.android.util.SessionUnavailableException;
import org.commcare.android.util.StringUtils;
import org.commcare.dalvik.R;
import org.commcare.dalvik.activities.CommCareHomeActivity;
import org.commcare.dalvik.application.CommCareApplication;
import org.commcare.dalvik.dialogs.AlertDialogFactory;
import org.commcare.dalvik.dialogs.CustomProgressDialog;
import org.commcare.dalvik.odk.provider.FormsProviderAPI.FormsColumns;
import org.commcare.dalvik.odk.provider.InstanceProviderAPI;
import org.commcare.dalvik.odk.provider.InstanceProviderAPI.InstanceColumns;
import org.commcare.dalvik.utils.UriToFilePath;
import org.javarosa.core.model.Constants;
import org.javarosa.core.model.FormIndex;
import org.javarosa.core.model.data.IAnswerData;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.locale.Localization;
import org.javarosa.core.services.locale.Localizer;
import org.javarosa.form.api.FormEntryController;
import org.javarosa.form.api.FormEntryPrompt;
import org.javarosa.model.xform.XFormsModule;
import org.javarosa.xpath.XPathException;
import org.javarosa.xpath.XPathTypeMismatchException;
import org.odk.collect.android.activities.components.FormNavigationController;
import org.odk.collect.android.activities.components.FormNavigationUI;
import org.odk.collect.android.activities.components.ImageCaptureProcessing;
import org.odk.collect.android.application.ODKStorage;
import org.odk.collect.android.jr.extensions.IntentCallout;
import org.odk.collect.android.jr.extensions.PollSensorAction;
import org.odk.collect.android.listeners.AdvanceToNextListener;
import org.odk.collect.android.listeners.FormSaveCallback;
import org.odk.collect.android.listeners.FormSavedListener;
import org.odk.collect.android.listeners.WidgetChangedListener;
import org.odk.collect.android.logic.FormController;
import org.odk.collect.android.logic.PropertyManager;
import org.odk.collect.android.preferences.FormEntryPreferences;
import org.odk.collect.android.tasks.FormLoaderTask;
import org.odk.collect.android.tasks.SaveToDiskTask;
import org.odk.collect.android.utilities.Base64Wrapper;
import org.odk.collect.android.utilities.FileUtils;
import org.odk.collect.android.utilities.GeoUtils;
import org.odk.collect.android.views.ODKView;
import org.odk.collect.android.views.ResizingImageView;
import org.odk.collect.android.widgets.DateTimeWidget;
import org.odk.collect.android.widgets.ImageWidget;
import org.odk.collect.android.widgets.IntentWidget;
import org.odk.collect.android.widgets.QuestionWidget;
import org.odk.collect.android.widgets.TimeWidget;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.crypto.spec.SecretKeySpec;

/**
 * Displays questions, animates transitions between
 * questions, and allows the user to enter data.
 * 
 * @author <NAME> (<EMAIL>)
 */
public class FormEntryActivity extends CommCareActivity<FormEntryActivity>
        implements AnimationListener, FormSavedListener, FormSaveCallback,
        AdvanceToNextListener, WidgetChangedListener {
    private static final String TAG = FormEntryActivity.class.getSimpleName();

    // Defines for FormEntryActivity
    private static final boolean EXIT = true;
    private static final boolean DO_NOT_EXIT = false;
    private static final boolean EVALUATE_CONSTRAINTS = true;
    private static final boolean DO_NOT_EVALUATE_CONSTRAINTS = false;

    // Request codes for returning data from specified intent.
    public static final int IMAGE_CAPTURE = 1;
    public static final int BARCODE_CAPTURE = 2;
    public static final int AUDIO_VIDEO_FETCH = 3;
    public static final int LOCATION_CAPTURE = 5;
    private static final int HIERARCHY_ACTIVITY = 6;
    public static final int IMAGE_CHOOSER = 7;
    private static final int FORM_PREFERENCES_KEY = 8;
    public static final int INTENT_CALLOUT = 10;
    private static final int HIERARCHY_ACTIVITY_FIRST_START = 11;
    public static final int SIGNATURE_CAPTURE = 12;

    // Extra returned from gp activity
    public static final String LOCATION_RESULT = "LOCATION_RESULT";

    // Identifies the gp of the form used to launch form entry
    private static final String KEY_FORMPATH = "formpath";
    public static final String KEY_INSTANCEDESTINATION = "instancedestination";
    public static final String TITLE_FRAGMENT_TAG = "odk_title_fragment";
    public static final String KEY_FORM_CONTENT_URI = "form_content_uri";
    public static final String KEY_INSTANCE_CONTENT_URI = "instance_content_uri";
    public static final String KEY_AES_STORAGE_KEY = "key_aes_storage";
    public static final String KEY_HEADER_STRING = "form_header";
    public static final String KEY_INCOMPLETE_ENABLED = "org.odk.collect.form.management";
    public static final String KEY_RESIZING_ENABLED = "org.odk.collect.resizing.enabled";
    private static final String KEY_HAS_SAVED = "org.odk.collect.form.has.saved";

    /**
     * Intent extra flag to track if this form is an archive. Used to trigger
     * return logic when this activity exits to the home screen, such as
     * whether to redirect to archive view or sync the form.
     */
    public static final String IS_ARCHIVED_FORM = "is-archive-form";

    // Identifies whether this is a new form, or reloading a form after a screen
    // rotation (or similar)
    private static final String KEY_FORM_LOAD_HAS_TRIGGERED = "newform";
    private static final String KEY_FORM_LOAD_FAILED = "form-failed";

    private static final int MENU_LANGUAGES = Menu.FIRST;
    private static final int MENU_HIERARCHY_VIEW = Menu.FIRST + 1;
    private static final int MENU_SAVE = Menu.FIRST + 2;
    private static final int MENU_PREFERENCES = Menu.FIRST + 3;

    public static final String NAV_STATE_NEXT = "next";

    private String mFormPath;
    // Path to a particular form instance
    public static String mInstancePath;
    private String mInstanceDestination;
    private GestureDetector mGestureDetector;
    
    private SecretKeySpec symetricKey = null;

    public static FormController mFormController;

    private Animation mInAnimation;
    private Animation mOutAnimation;

    private ViewGroup mViewPane;
    private View mCurrentView;

    private boolean mIncompleteEnabled = true;
    private boolean hasFormLoadBeenTriggered = false;
    private boolean hasFormLoadFailed = false;

    // used to limit forward/backward swipes to one per question
    private boolean mBeenSwiped;

    private FormLoaderTask<FormEntryActivity> mFormLoaderTask;
    private SaveToDiskTask<FormEntryActivity> mSaveToDiskTask;
    
    private Uri formProviderContentURI = FormsColumns.CONTENT_URI;
    private Uri instanceProviderContentURI = InstanceColumns.CONTENT_URI;
    
    private static String mHeaderString;
    
    // Was the form saved? Used to set activity return code.
    private boolean hasSaved = false;

    private BroadcastReceiver mLocationServiceIssueReceiver;

    // marked true if we are in the process of saving a form because the user
    // database & key session are expiring. Being set causes savingComplete to
    // broadcast a form saving intent.
    private boolean savingFormOnKeySessionExpiration = false;

    enum AnimationType {
        LEFT, RIGHT, FADE
    }

    @Override
    @SuppressLint("NewApi")
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        addBreadcrumbBar();

        // must be at the beginning of any activity that can be called from an external intent
        try {
            ODKStorage.createODKDirs();
        } catch (RuntimeException e) {
            Logger.exception(e);
            CommCareActivity.createErrorDialog(this, e.getMessage(), EXIT);
            return;
        }

        setupUI();

        // Load JavaRosa modules. needed to restore forms.
        new XFormsModule().registerModule();

        // needed to override rms property manager
        org.javarosa.core.services.PropertyManager.setPropertyManager(new PropertyManager(
                getApplicationContext()));

        loadStateFromBundle(savedInstanceState);

        // Check to see if this is a screen flip or a new form load.
        Object data = this.getLastCustomNonConfigurationInstance();
        if (data instanceof FormLoaderTask) {
            mFormLoaderTask = (FormLoaderTask) data;
        } else if (data instanceof SaveToDiskTask) {
            mSaveToDiskTask = (SaveToDiskTask) data;
            mSaveToDiskTask.setFormSavedListener(this);
        } else if (hasFormLoadBeenTriggered && !hasFormLoadFailed) {
            // Screen orientation change
            refreshCurrentView();
        }
    }

    @Override
    public boolean onMenuItemSelected(int featureId, MenuItem item) {
        /*
         * EventLog accepts only proper Strings as input, but prior to this version,
         * Android would try to send SpannedStrings to it, thus crashing the app.
         * This makes sure the title is actually a String.
         * This fixes bug 174626.
         */
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2
                && item.getTitleCondensed() != null) {
            item.setTitleCondensed(item.getTitleCondensed().toString());
        }

        return super.onMenuItemSelected(featureId, item);
    }

    @Override
    public void formSaveCallback() {
        // note that we have started saving the form
        savingFormOnKeySessionExpiration = true;
        // start saving form, which will call the key session logout completion
        // function when it finishes.
        saveIncompleteFormToDisk(EXIT, null, true);
    }

    private void registerFormEntryReceiver() {
        //BroadcastReceiver for:
        // a) An unresolvable xpath expression encountered in PollSensorAction.onLocationChanged
        // b) Checking if GPS services are not available
        mLocationServiceIssueReceiver = new BroadcastReceiver() {

            @Override
            public void onReceive(Context context, Intent intent) {
                context.removeStickyBroadcast(intent);
                String action = intent.getAction();
                if (GeoUtils.ACTION_CHECK_GPS_ENABLED.equals(action)) {
                    handleNoGpsBroadcast(context);
                } else if (PollSensorAction.XPATH_ERROR_ACTION.equals(action)) {
                    handleXpathErrorBroadcast(intent);
                }
            }
        };

        IntentFilter filter = new IntentFilter();
        filter.addAction(PollSensorAction.XPATH_ERROR_ACTION);
        filter.addAction(GeoUtils.ACTION_CHECK_GPS_ENABLED);
        registerReceiver(mLocationServiceIssueReceiver, filter);
    }

    private void handleNoGpsBroadcast(Context context) {
        LocationManager manager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
        Set<String> providers = GeoUtils.evaluateProviders(manager);
        if (providers.isEmpty()) {
            DialogInterface.OnClickListener onChangeListener = new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int i) {
                    if (i == DialogInterface.BUTTON_POSITIVE) {
                        Intent intent = new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
                        startActivity(intent);
                    }
                }
            };
            GeoUtils.showNoGpsDialog(this, onChangeListener);
        }
    }

    private void handleXpathErrorBroadcast(Intent intent) {
        String problemXpath = intent.getStringExtra(PollSensorAction.KEY_UNRESOLVED_XPATH);
        CommCareActivity.createErrorDialog(FormEntryActivity.this,
                "There is a bug in one of your form's XPath Expressions \n" + problemXpath, EXIT);
    }

    private void setupUI() {
        setContentView(R.layout.screen_form_entry);

        ImageButton nextButton = (ImageButton)this.findViewById(R.id.nav_btn_next);
        ImageButton prevButton = (ImageButton)this.findViewById(R.id.nav_btn_prev);

        nextButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!"done".equals(v.getTag())) {
                    FormEntryActivity.this.showNextView();
                } else {
                    triggerUserFormComplete();
                }
            }
        });

        prevButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!"quit".equals(v.getTag())) {
                    FormEntryActivity.this.showPreviousView(true);
                } else {
                    FormEntryActivity.this.triggerUserQuitInput();
                }
            }
        });

        mViewPane = (ViewGroup)findViewById(R.id.form_entry_pane);

        mBeenSwiped = false;
        mCurrentView = null;
        mInAnimation = null;
        mOutAnimation = null;
        mGestureDetector = new GestureDetector(this);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString(KEY_FORMPATH, mFormPath);
        outState.putBoolean(KEY_FORM_LOAD_HAS_TRIGGERED, hasFormLoadBeenTriggered);
        outState.putBoolean(KEY_FORM_LOAD_FAILED, hasFormLoadFailed);
        outState.putString(KEY_FORM_CONTENT_URI, formProviderContentURI.toString());
        outState.putString(KEY_INSTANCE_CONTENT_URI, instanceProviderContentURI.toString());
        outState.putString(KEY_INSTANCEDESTINATION, mInstanceDestination);
        outState.putBoolean(KEY_INCOMPLETE_ENABLED, mIncompleteEnabled);
        outState.putBoolean(KEY_HAS_SAVED, hasSaved);
        outState.putString(KEY_RESIZING_ENABLED, ResizingImageView.resizeMethod);
        
        if(symetricKey != null) {
            try {
                outState.putString(KEY_AES_STORAGE_KEY, new Base64Wrapper().encodeToString(symetricKey.getEncoded()));
            } catch (ClassNotFoundException e) {
                // we can't really get here anyway, since we couldn't have decoded the string to begin with
                throw new RuntimeException("Base 64 encoding unavailable! Can't pass storage key");
            }
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
        super.onActivityResult(requestCode, resultCode, intent);

        if (requestCode == FORM_PREFERENCES_KEY) {
            refreshCurrentView(false);
            return;
        }

        if (resultCode == RESULT_CANCELED) {
            if (requestCode == HIERARCHY_ACTIVITY_FIRST_START) {
                // They pressed 'back' on the first hierarchy screen, so we should assume they want
                // to back out of form entry all together
                finishReturnInstance(false);
            } else if (requestCode == INTENT_CALLOUT){
                processIntentResponse(intent, true);
            }
            // request was canceled, so do nothing
            return;
        }

        switch (requestCode) {
            case BARCODE_CAPTURE:
                String sb = intent.getStringExtra(BarcodeScanListenerDefaultImpl.SCAN_RESULT);
                ((ODKView) mCurrentView).setBinaryData(sb);
                saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
                break;
            case INTENT_CALLOUT:
                processIntentResponse(intent);
                break;
            case IMAGE_CAPTURE:
                processCaptureResponse(true);
                break;
            case SIGNATURE_CAPTURE:
                processCaptureResponse(false);
                break;
            case IMAGE_CHOOSER:
                processImageChooserResponse(intent);
                break;
            case AUDIO_VIDEO_FETCH:
                processChooserResponse(intent);
                break;
            case LOCATION_CAPTURE:
                String sl = intent.getStringExtra(LOCATION_RESULT);
                ((ODKView) mCurrentView).setBinaryData(sl);
                saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
                break;
            case HIERARCHY_ACTIVITY:
            case HIERARCHY_ACTIVITY_FIRST_START:
                // We may have jumped to a new index in hierarchy activity, so refresh
                refreshCurrentView(false);
                break;
        }
    }

    /**
     * Processes the return from an image capture intent, launched by either an ImageWidget or
     * SignatureWidget
     *
     * @param isImage true if this was from an ImageWidget, false if it was a SignatureWidget
     */
    private void processCaptureResponse(boolean isImage) {
        /* We saved the image to the tempfile_path, but we really want it to be in:
         * /sdcard/odk/instances/[current instance]/something.[jpg/png/etc] so we move it there
         * before inserting it into the content provider. Once the android image capture bug gets
         * fixed, (read, we move on from Android 1.6) we want to handle images the audio and
         * video
         */

        // The intent is empty, but we know we saved the image to the temp file
        File originalImage = ImageWidget.TEMP_FILE_FOR_IMAGE_CAPTURE;
        try {
            File unscaledFinalImage = ImageCaptureProcessing.moveAndScaleImage(originalImage, isImage, getInstanceFolder(), this);
            saveImageWidgetAnswer(unscaledFinalImage);
        } catch (IOException e) {
            e.printStackTrace();
            showCustomToast(Localization.get("image.capture.not.saved"), Toast.LENGTH_LONG);
        }
    }

    private String getInstanceFolder() {
       return mInstancePath.substring(0, mInstancePath.lastIndexOf("/") + 1);
    }

    private void processImageChooserResponse(Intent intent) {
        /* We have a saved image somewhere, but we really want it to be in:
         * /sdcard/odk/instances/[current instance]/something.[jpg/png/etc] so we move it there
         * before inserting it into the content provider. Once the android image capture bug gets
         * fixed, (read, we move on from Android 1.6) we want to handle images the audio and
         * video
         */

        // get gp of chosen file
        Uri selectedImage = intent.getData();
        File originalImage = new File(FileUtils.getPath(this, selectedImage));

        if (originalImage.exists()) {
            try {
                File unscaledFinalImage = ImageCaptureProcessing.moveAndScaleImage(originalImage, true, getInstanceFolder(), this);
                saveImageWidgetAnswer(unscaledFinalImage);
            } catch (IOException e) {
                e.printStackTrace();
                showCustomToast(Localization.get("image.selection.not.saved"), Toast.LENGTH_LONG);
            }
        } else {
            // The user has managed to select a file from the image browser that doesn't actually
            // exist on the file system anymore
            showCustomToast(Localization.get("invalid.image.selection"), Toast.LENGTH_LONG);
        }
    }

    private void saveImageWidgetAnswer(File unscaledFinalImage) {
        // Add the new image to the Media content provider so that the viewing is fast in Android 2.0+
        ContentValues values = new ContentValues(6);
        values.put(Images.Media.TITLE, unscaledFinalImage.getName());
        values.put(Images.Media.DISPLAY_NAME, unscaledFinalImage.getName());
        values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis());
        values.put(Images.Media.MIME_TYPE, "image/jpeg");
        values.put(Images.Media.DATA, unscaledFinalImage.getAbsolutePath());

        Uri imageURI =
                getContentResolver().insert(Images.Media.EXTERNAL_CONTENT_URI, values);
        Log.i(TAG, "Inserting image returned uri = " + imageURI.toString());

        ((ODKView) mCurrentView).setBinaryData(imageURI);
        saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
        refreshCurrentView();
    }

    private void processChooserResponse(Intent intent) {
        // For audio/video capture/chooser, we get the URI from the content provider
        // then the widget copies the file and makes a new entry in the content provider.
        Uri media = intent.getData();
        String binaryPath = UriToFilePath.getPathFromUri(CommCareApplication._(), media);
        if (!FormUploadUtil.isSupportedMultimediaFile(binaryPath)) {
            // don't let the user select a file that won't be included in the
            // upload to the server
            ((ODKView) mCurrentView).clearAnswer();
            Toast.makeText(FormEntryActivity.this,
                    Localization.get("form.attachment.invalid"),
                    Toast.LENGTH_LONG).show();
        } else {
            ((ODKView) mCurrentView).setBinaryData(media);
        }
        saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
        refreshCurrentView();
    }

    /**
     * Search the the current view's widgets for one that has registered a
     * pending callout with the form controller
     */
    public QuestionWidget getPendingWidget() {
        FormIndex pendingIndex = mFormController.getPendingCalloutFormIndex();
        if (pendingIndex == null) {
            Logger.log(AndroidLogger.SOFT_ASSERT,
                    "getPendingWidget called when pending callout form index was null");
            return null;
        }
        for (QuestionWidget q : ((ODKView)mCurrentView).getWidgets()) {
            if (q.getFormId().equals(pendingIndex)) {
                return q;
            }
        }
        Logger.log(AndroidLogger.SOFT_ASSERT,
                "getPendingWidget couldn't find question widget with a form index that matches the pending callout.");
        return null;
    }

    private void processIntentResponse(Intent response){
        processIntentResponse(response, false);
    }

    private void processIntentResponse(Intent response, boolean cancelled) {
        // keep track of whether we should auto advance
        boolean advance = false;
        boolean quick = false;

        IntentWidget pendingIntentWidget = (IntentWidget)getPendingWidget();
        TreeReference context;
        if (mFormController.getPendingCalloutFormIndex() != null) {
            context = mFormController.getPendingCalloutFormIndex().getReference();
        } else {
            context = null;
        }
        if(pendingIntentWidget != null) {
            //Set our instance destination for binary data if needed
            String destination = mInstancePath.substring(0, mInstancePath.lastIndexOf("/") + 1);
            
            //get the original intent callout
            IntentCallout ic = pendingIntentWidget.getIntentCallout();
            
            quick = "quick".equals(ic.getAppearance());

            //And process it 
            advance = ic.processResponse(response, context, new File(destination));
            
            ic.setCancelled(cancelled);
        }

        refreshCurrentView();

        // auto advance if we got a good result and are in quick mode
        if(advance && quick){
            showNextView();
        }
    }

    private void updateFormRelevencies(){
        saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
        
        if(!(mCurrentView instanceof ODKView)){
            throw new RuntimeException("Tried to update form relevency not on compound view");
        }
        
        ODKView oldODKV = (ODKView)mCurrentView;
        
        FormEntryPrompt[] newValidPrompts = mFormController.getQuestionPrompts();
        Set<FormEntryPrompt> used = new HashSet<>();
        
        ArrayList<QuestionWidget> oldWidgets = oldODKV.getWidgets();

        ArrayList<Integer> removeList = new ArrayList<>();

           for(int i=0;i<oldWidgets.size();i++){
            QuestionWidget oldWidget = oldWidgets.get(i);
            boolean stillRelevent = false;

            for(FormEntryPrompt prompt : newValidPrompts) {
            	if(prompt.getIndex().equals(oldWidget.getPrompt().getIndex())) {
            		stillRelevent = true;
            		used.add(prompt);
            	}
            }
            if(!stillRelevent){
                removeList.add(i);
            }
        }
        // remove "atomically" to not mess up iterations
        oldODKV.removeQuestionsFromIndex(removeList);

        //Now go through add add any new prompts that we need
        for(int i = 0 ; i < newValidPrompts.length; ++i) {
        	FormEntryPrompt prompt = newValidPrompts[i]; 
        	if(used.contains(prompt)) {
        		continue;
        	} 
        	oldODKV.addQuestionToIndex(prompt, mFormController.getWidgetFactory(), i);
        }
    }

	/**
     * 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.
     */
    private void refreshCurrentView(boolean animateLastView) {
        if(mFormController == null) { throw new RuntimeException("Form state is lost! Cannot refresh current view. This shouldn't happen, please submit a bug report."); }
        int event = 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 && !mFormController.indexIsInFieldList())
                || event == FormEntryController.EVENT_REPEAT
                || (mFormController.indexIsInFieldList() && !(event == FormEntryController.EVENT_GROUP))) {
            event = 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 {
            View current = createView();
            showView(current, AnimationType.FADE, animateLastView);
        }
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        menu.removeItem(MENU_LANGUAGES);
        menu.removeItem(MENU_HIERARCHY_VIEW);
        menu.removeItem(MENU_SAVE);
        menu.removeItem(MENU_PREFERENCES);

        if(mIncompleteEnabled) {
            menu.add(0, MENU_SAVE, 0, StringUtils.getStringRobust(this, R.string.save_all_answers)).setIcon(
                    android.R.drawable.ic_menu_save);
        }
        menu.add(0, MENU_HIERARCHY_VIEW, 0, StringUtils.getStringRobust(this, R.string.view_hierarchy)).setIcon(
                R.drawable.ic_menu_goto);

        boolean hasMultipleLanguages =
                (!(mFormController == null || mFormController.getLanguages() == null || mFormController.getLanguages().length == 1));
        menu.add(0, MENU_LANGUAGES, 0, StringUtils.getStringRobust(this, R.string.change_language))
                .setIcon(R.drawable.ic_menu_start_conversation)
                .setEnabled(hasMultipleLanguages);

        
        menu.add(0, MENU_PREFERENCES, 0, StringUtils.getStringRobust(this, R.string.form_entry_settings)).setIcon(
                android.R.drawable.ic_menu_preferences);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case MENU_LANGUAGES:
                createLanguageDialog();
                return true;
            case MENU_SAVE:
                saveFormToDisk(DO_NOT_EXIT, null, false);
                return true;
            case MENU_HIERARCHY_VIEW:
                if (currentPromptIsQuestion()) {
                    saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
                }
                Intent i = new Intent(this, FormHierarchyActivity.class);
                startActivityForResult(i, HIERARCHY_ACTIVITY);
                return true;
            case MENU_PREFERENCES:
                Intent pref = new Intent(this, FormEntryPreferences.class);
                startActivityForResult(pref, FORM_PREFERENCES_KEY);
                return true;
            case android.R.id.home:
                triggerUserQuitInput();
                return true;

        }
        return super.onOptionsItemSelected(item);
    }

    /**
     * @return true If the current index of the form controller contains questions
     */
    private boolean currentPromptIsQuestion() {
        return (mFormController.getEvent() == FormEntryController.EVENT_QUESTION || mFormController
                .getEvent() == FormEntryController.EVENT_GROUP);
    }

    private boolean saveAnswersForCurrentScreen(boolean evaluateConstraints) {
        return saveAnswersForCurrentScreen(evaluateConstraints, true, false);
    }

    /**
     * Attempt to save the answer(s) in the current screen to into the data model.
     *
     * @param failOnRequired      Whether or not the constraint evaluation
     *                            should return false if the question is only
     *                            required. (this is helpful for incomplete
     *                            saves)
     * @param headless            running in a process that can't display graphics
     * @return false if any error occurs while saving (constraint violated,
     * etc...), true otherwise.
     */
    private boolean saveAnswersForCurrentScreen(boolean evaluateConstraints,
                                                boolean failOnRequired,
                                                boolean headless) {
        // only try to save if the current event is a question or a field-list
        // group
        boolean success = true;
        if ((mFormController.getEvent() == FormEntryController.EVENT_QUESTION)
                || ((mFormController.getEvent() == FormEntryController.EVENT_GROUP) &&
                mFormController.indexIsInFieldList())) {
            if (mCurrentView instanceof ODKView) {
                HashMap<FormIndex, IAnswerData> answers =
                        ((ODKView)mCurrentView).getAnswers();

                // Sort the answers so if there are multiple errors, we can
                // bring focus to the first one
                List<FormIndex> indexKeys = new ArrayList<>();
                indexKeys.addAll(answers.keySet());
                Collections.sort(indexKeys, new Comparator<FormIndex>() {
                    @Override
                    public int compare(FormIndex arg0, FormIndex arg1) {
                        return arg0.compareTo(arg1);
                    }
                });

                for (FormIndex index : indexKeys) {
                    // Within a group, you can only save for question events
                    if (mFormController.getEvent(index) == FormEntryController.EVENT_QUESTION) {
                        int saveStatus = saveAnswer(answers.get(index),
                                index, evaluateConstraints);
                        if (evaluateConstraints &&
                                ((saveStatus != FormEntryController.ANSWER_OK) &&
                                        (failOnRequired ||
                                                saveStatus != FormEntryController.ANSWER_REQUIRED_BUT_EMPTY))) {
                            if (!headless) {
                                createConstraintToast(index, mFormController.getQuestionPrompt(index).getConstraintText(), saveStatus, success);
                            }
                            success = false;
                        }
                    } else {
                        Log.w(TAG,
                                "Attempted to save an index referencing something other than a question: "
                                        + index.getReference());
                    }
                }
            } else {
                String viewType;
                if (mCurrentView == null || mCurrentView.getClass() == null) {
                   viewType = "null";
                } else {
                    viewType = mCurrentView.getClass().toString();
                }
                Log.w(TAG, "Unknown view type rendered while current event was question or group! View type: " + viewType);
            }
        }
        return success;
    }

    /**
     * Clears the answer on the screen.
     */
    private void clearAnswer(QuestionWidget qw) {
        qw.clearAnswer();
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);
        menu.add(0, v.getId(), 0, StringUtils.getStringSpannableRobust(this, R.string.clear_answer));
        menu.setHeaderTitle(StringUtils.getStringSpannableRobust(this, R.string.edit_prompt));
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        // We don't have the right view here, so we store the View's ID as the
        // item ID and loop through the possible views to find the one the user
        // clicked on.
        for (QuestionWidget qw : ((ODKView) mCurrentView).getWidgets()) {
            if (item.getItemId() == qw.getId()) {
                createClearDialog(qw);
            }
        }

        return super.onContextItemSelected(item);
    }

    /**
     * If we're loading, then we pass the loading thread to our next instance.
     */
    @Override
    public Object onRetainCustomNonConfigurationInstance() {
        // if a form is loading, pass the loader task
        if (mFormLoaderTask != null && mFormLoaderTask.getStatus() != AsyncTask.Status.FINISHED)
            return mFormLoaderTask;

        // if a form is writing to disk, pass the save to disk task
        if (mSaveToDiskTask != null && mSaveToDiskTask.getStatus() != AsyncTask.Status.FINISHED)
            return mSaveToDiskTask;

        // mFormEntryController is static so we don't need to pass it.
        if (mFormController != null && currentPromptIsQuestion()) {
            saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
        }
        return null;
    }
    
    private String getHeaderString() {
        if(mHeaderString != null) {
            //Localization?
            return mHeaderString;
        } else {
            return StringUtils.getStringRobust(this, R.string.application_name) + " > " + mFormController.getFormTitle();
        }
    }

    private View createView() {
        setTitle(getHeaderString());
        ODKView odkv;
        // should only be a group here if the event_group is a field-list
        try {
            odkv =
                    new ODKView(this, mFormController.getQuestionPrompts(),
                            mFormController.getGroupsForCurrentIndex(),
                            mFormController.getWidgetFactory(), this);
            Log.i(TAG, "created view for group");
        } catch (RuntimeException e) {
            Logger.exception(e);
            CommCareActivity.createErrorDialog(this, e.getMessage(), 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 View(this);
        }

        // 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() &&
                    !mFormController.isFormReadOnly() &&
                    (qw.getPrompt().getControlType() == Constants.CONTROL_SELECT_ONE ||
                            qw.getPrompt().getControlType() == Constants.CONTROL_SELECT_MULTI)) {
                registerForContextMenu(qw);
            }
        }

        FormNavigationUI formNavUi = new FormNavigationUI(this, mCurrentView, mFormController);
        formNavUi.updateNavigationCues(odkv);

        return odkv;
    }

    @SuppressLint("NewApi")
    @Override
    public boolean dispatchTouchEvent(MotionEvent mv) {
        //We need to ignore this even if it's processed by the action
        //bar (if one exists)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            ActionBar bar = getActionBar();
            if (bar != null) {
                View customView = bar.getCustomView();
                if (customView != null && customView.dispatchTouchEvent(mv)) {
                    return true;
                }
            }
        }

        boolean handled = mGestureDetector.onTouchEvent(mv);
        return handled || super.dispatchTouchEvent(mv);
    }

    /**
     * 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.
     */
    private void showNextView() { showNextView(false); }
    private void showNextView(boolean resuming) {
        if (currentPromptIsQuestion()) {
            if (!saveAnswersForCurrentScreen(EVALUATE_CONSTRAINTS)) {
                // A constraint was violated so a dialog should be showing.
                return;
            }
        }

        if (mFormController.getEvent() != FormEntryController.EVENT_END_OF_FORM) {
            int event;
            
            try{
            group_skip: do {
                event = mFormController.stepToNextEvent(FormController.STEP_OVER_GROUP);
                switch (event) {
                    case FormEntryController.EVENT_QUESTION:
                        View next = createView();
                        if (!resuming) {
                            showView(next, AnimationType.RIGHT);
                        } else {
                            showView(next, AnimationType.FADE, false);
                        }
                        break group_skip;
                    case FormEntryController.EVENT_END_OF_FORM:
                        Logger.log(AndroidLogger.SOFT_ASSERT,
                                "Trying to show an end of form event");
                        CommCareActivity.createErrorDialog(this, "Unexpectedly reached the end of the form.", EXIT);
                        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 (mFormController.indexIsInFieldList()
                                && mFormController.getQuestionPrompts().length != 0) {
                            View nextGroupView = createView();
                            if(!resuming) {
                                showView(nextGroupView, AnimationType.RIGHT);
{'fraction_non_alphanumeric': 0.07965826933771936, 'fraction_numerical': 0.0014317662789459361, 'mean_word_length': 2.3745408081776076, 'pattern_counts': {'":': 0, '<': 39, '<?xml version=': 0, '>': 37, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 26, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3109033', 'n_tokens_mistral': 22454, 'n_tokens_neox': 21563, 'n_words': 5568}