org.odk.collect.android.activities.FormEntryActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.odk.collect.android.activities.FormEntryActivity.java

Source

/*
 * Copyright (C) 2009 University of Washington
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package org.odk.collect.android.activities;

import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.MediaStore.Images;
import android.support.v4.view.ViewConfigurationCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.InputFilter;
import android.text.Spanned;
import android.util.DisplayMetrics;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
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.ViewConfiguration;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.InputMethodManager;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;

import org.javarosa.core.model.FormIndex;
import org.javarosa.core.model.data.IAnswerData;
import org.javarosa.core.model.instance.TreeElement;
import org.javarosa.form.api.FormEntryCaption;
import org.javarosa.form.api.FormEntryController;
import org.javarosa.form.api.FormEntryPrompt;
import org.odk.collect.android.R;
import org.odk.collect.android.application.Collect;
import org.odk.collect.android.dao.FormsDao;
import org.odk.collect.android.dao.InstancesDao;
import org.odk.collect.android.exception.GDriveConnectionException;
import org.odk.collect.android.exception.JavaRosaException;
import org.odk.collect.android.listeners.AdvanceToNextListener;
import org.odk.collect.android.listeners.FormLoaderListener;
import org.odk.collect.android.listeners.FormSavedListener;
import org.odk.collect.android.listeners.SavePointListener;
import org.odk.collect.android.logic.FormController;
import org.odk.collect.android.logic.FormController.FailedConstraint;
import org.odk.collect.android.preferences.AdminKeys;
import org.odk.collect.android.preferences.AdminPreferencesActivity;
import org.odk.collect.android.preferences.PreferenceKeys;
import org.odk.collect.android.preferences.PreferencesActivity;
import org.odk.collect.android.provider.FormsProviderAPI.FormsColumns;
import org.odk.collect.android.provider.InstanceProviderAPI;
import org.odk.collect.android.provider.InstanceProviderAPI.InstanceColumns;
import org.odk.collect.android.tasks.FormLoaderTask;
import org.odk.collect.android.tasks.SavePointTask;
import org.odk.collect.android.tasks.SaveResult;
import org.odk.collect.android.tasks.SaveToDiskTask;
import org.odk.collect.android.utilities.ApplicationConstants;
import org.odk.collect.android.utilities.FileUtils;
import org.odk.collect.android.utilities.MediaUtils;
import org.odk.collect.android.utilities.ToastUtils;
import org.odk.collect.android.views.ODKView;
import org.odk.collect.android.widgets.QuestionWidget;
import org.odk.collect.android.widgets.StringWidget;

import java.io.File;
import java.io.FileFilter;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;

import timber.log.Timber;

/**
 * FormEntryActivity is responsible for displaying questions, animating
 * transitions between questions, and allowing the user to enter data.
 *
 * @author Carl Hartung (carlhartung@gmail.com)
 * @author Thomas Smyth, Sassafras Tech Collective (tom@sassafrastech.com; constraint behavior
 *         option)
 */
public class FormEntryActivity extends AppCompatActivity implements AnimationListener, FormLoaderListener,
        FormSavedListener, AdvanceToNextListener, OnGestureListener, SavePointListener {

    // save with every swipe forward or back. Timings indicate this takes .25
    // seconds.
    // if it ever becomes an issue, this value can be changed to save every n'th
    // screen.
    private static final int SAVEPOINT_INTERVAL = 1;

    // 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_CAPTURE = 3;
    public static final int VIDEO_CAPTURE = 4;
    public static final int LOCATION_CAPTURE = 5;
    public static final int HIERARCHY_ACTIVITY = 6;
    public static final int IMAGE_CHOOSER = 7;
    public static final int AUDIO_CHOOSER = 8;
    public static final int VIDEO_CHOOSER = 9;
    public static final int EX_STRING_CAPTURE = 10;
    public static final int EX_INT_CAPTURE = 11;
    public static final int EX_DECIMAL_CAPTURE = 12;
    public static final int DRAW_IMAGE = 13;
    public static final int SIGNATURE_CAPTURE = 14;
    public static final int ANNOTATE_IMAGE = 15;
    public static final int ALIGNED_IMAGE = 16;
    public static final int BEARING_CAPTURE = 17;
    public static final int EX_GROUP_CAPTURE = 18;
    public static final int OSM_CAPTURE = 19;
    public static final int GEOSHAPE_CAPTURE = 20;
    public static final int GEOTRACE_CAPTURE = 21;

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

    public static final String KEY_INSTANCES = "instances";
    public static final String KEY_SUCCESS = "success";
    public static final String KEY_ERROR = "error";

    // Identifies the gp of the form used to launch form entry
    public static final String KEY_FORMPATH = "formpath";

    // Identifies whether this is a new form, or reloading a form after a screen
    // rotation (or similar)
    private static final String NEWFORM = "newform";
    // these are only processed if we shut down and are restoring after an
    // external intent fires

    public static final String KEY_INSTANCEPATH = "instancepath";
    public static final String KEY_XPATH = "xpath";
    public static final String KEY_XPATH_WAITING_FOR_DATA = "xpathwaiting";

    // Tracks whether we are autosaving
    public static final String KEY_AUTO_SAVED = "autosaved";

    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;

    private static final int PROGRESS_DIALOG = 1;
    private static final int SAVING_DIALOG = 2;
    private static final int SAVING_IMAGE_DIALOG = 3;

    private boolean autoSaved;
    private boolean doSwipe = true;

    // Random ID
    private static final int DELETE_REPEAT = 654321;

    private String formPath;
    private GestureDetector gestureDetector;

    private Animation inAnimation;
    private Animation outAnimation;
    private View staleView = null;

    private LinearLayout questionHolder;
    private View currentView;

    private AlertDialog alertDialog;
    private ProgressDialog progressDialog;
    private String errorMessage;
    private boolean shownAlertDialogIsGroupRepeat;

    // used to limit forward/backward swipes to one per question
    private boolean beenSwiped = false;

    private final Object saveDialogLock = new Object();
    private int viewCount = 0;

    private FormLoaderTask formLoaderTask;
    private SaveToDiskTask saveToDiskTask;

    private ImageButton nextButton;
    private ImageButton backButton;

    private String stepMessage = "";
    private Toolbar toolbar;
    private boolean hasHardwareMenu;

    enum AnimationType {
        LEFT, RIGHT, FADE
    }

    private SharedPreferences adminPreferences;
    private boolean showNavigationButtons = false;

    private FormsDao formsDao;

    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

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

        setContentView(R.layout.form_entry);

        formsDao = new FormsDao();

        errorMessage = null;

        beenSwiped = false;
        alertDialog = null;
        currentView = null;
        inAnimation = null;
        outAnimation = null;
        gestureDetector = new GestureDetector(this, this);
        questionHolder = (LinearLayout) findViewById(R.id.questionholder);

        hasHardwareMenu = ViewConfigurationCompat
                .hasPermanentMenuKey(ViewConfiguration.get(getApplicationContext()));

        // get admin preference settings
        adminPreferences = getSharedPreferences(AdminPreferencesActivity.ADMIN_PREFERENCES, 0);

        initToolbar();

        nextButton = (ImageButton) findViewById(R.id.form_forward_button);
        nextButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                beenSwiped = true;
                showNextView();
            }
        });

        backButton = (ImageButton) findViewById(R.id.form_back_button);
        backButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                beenSwiped = true;
                showPreviousView();
            }
        });

        String startingXPath = null;
        String waitingXPath = null;
        String instancePath = null;
        Boolean newForm = true;
        autoSaved = false;
        if (savedInstanceState != null) {
            if (savedInstanceState.containsKey(KEY_FORMPATH)) {
                formPath = savedInstanceState.getString(KEY_FORMPATH);
            }
            if (savedInstanceState.containsKey(KEY_INSTANCEPATH)) {
                instancePath = savedInstanceState.getString(KEY_INSTANCEPATH);
            }
            if (savedInstanceState.containsKey(KEY_XPATH)) {
                startingXPath = savedInstanceState.getString(KEY_XPATH);
                Timber.i("startingXPath is: %s", startingXPath);
            }
            if (savedInstanceState.containsKey(KEY_XPATH_WAITING_FOR_DATA)) {
                waitingXPath = savedInstanceState.getString(KEY_XPATH_WAITING_FOR_DATA);
                Timber.i("waitingXPath is: %s", waitingXPath);
            }
            if (savedInstanceState.containsKey(NEWFORM)) {
                newForm = savedInstanceState.getBoolean(NEWFORM, true);
            }
            if (savedInstanceState.containsKey(KEY_ERROR)) {
                errorMessage = savedInstanceState.getString(KEY_ERROR);
            }
            if (savedInstanceState.containsKey(KEY_AUTO_SAVED)) {
                autoSaved = savedInstanceState.getBoolean(KEY_AUTO_SAVED);
            }
        }

        // If a parse error message is showing then nothing else is loaded
        // Dialogs mid form just disappear on rotation.
        if (errorMessage != null) {
            createErrorDialog(errorMessage, EXIT);
            return;
        }

        // Check to see if this is a screen flip or a new form load.
        Object data = getLastCustomNonConfigurationInstance();
        if (data instanceof FormLoaderTask) {
            formLoaderTask = (FormLoaderTask) data;
        } else if (data instanceof SaveToDiskTask) {
            saveToDiskTask = (SaveToDiskTask) data;
        } else if (data == null) {
            if (!newForm) {
                if (Collect.getInstance().getFormController() != null) {
                    refreshCurrentView();
                } else {
                    Timber.w("Reloading form and restoring state.");
                    // we need to launch the form loader to load the form
                    // controller...
                    formLoaderTask = new FormLoaderTask(instancePath, startingXPath, waitingXPath);
                    Collect.getInstance().getActivityLogger().logAction(this, "formReloaded", formPath);
                    // TODO: this doesn' work (dialog does not get removed):
                    // showDialog(PROGRESS_DIALOG);
                    // show dialog before we execute...
                    formLoaderTask.execute(formPath);
                }
                return;
            }

            // Not a restart from a screen orientation change (or other).
            Collect.getInstance().setFormController(null);
            supportInvalidateOptionsMenu();

            Intent intent = getIntent();
            if (intent != null) {
                Uri uri = intent.getData();

                if (uri != null && getContentResolver().getType(uri).equals(InstanceColumns.CONTENT_ITEM_TYPE)) {
                    // get the formId and version for this instance...
                    String jrFormId = null;
                    String jrVersion = null;
                    {
                        Cursor instanceCursor = null;
                        try {
                            instanceCursor = getContentResolver().query(uri, null, null, null, null);
                            if (instanceCursor.getCount() != 1) {
                                this.createErrorDialog(getString(R.string.bad_uri, uri), EXIT);
                                return;
                            } else {
                                instanceCursor.moveToFirst();
                                instancePath = instanceCursor.getString(
                                        instanceCursor.getColumnIndex(InstanceColumns.INSTANCE_FILE_PATH));
                                Collect.getInstance().getActivityLogger().logAction(this, "instanceLoaded",
                                        instancePath);

                                jrFormId = instanceCursor
                                        .getString(instanceCursor.getColumnIndex(InstanceColumns.JR_FORM_ID));
                                int idxJrVersion = instanceCursor.getColumnIndex(InstanceColumns.JR_VERSION);

                                jrVersion = instanceCursor.isNull(idxJrVersion) ? null
                                        : instanceCursor.getString(idxJrVersion);
                            }
                        } finally {
                            if (instanceCursor != null) {
                                instanceCursor.close();
                            }
                        }
                    }

                    String[] selectionArgs;
                    String selection;

                    if (jrVersion == null) {
                        selectionArgs = new String[] { jrFormId };
                        selection = FormsColumns.JR_FORM_ID + "=? AND " + FormsColumns.JR_VERSION + " IS NULL";
                    } else {
                        selectionArgs = new String[] { jrFormId, jrVersion };
                        selection = FormsColumns.JR_FORM_ID + "=? AND " + FormsColumns.JR_VERSION + "=?";
                    }

                    {
                        Cursor formCursor = null;
                        try {
                            formCursor = formsDao.getFormsCursor(selection, selectionArgs);
                            if (formCursor.getCount() == 1) {
                                formCursor.moveToFirst();
                                formPath = formCursor
                                        .getString(formCursor.getColumnIndex(FormsColumns.FORM_FILE_PATH));
                            } else if (formCursor.getCount() < 1) {
                                this.createErrorDialog(
                                        getString(R.string.parent_form_not_present, jrFormId)
                                                + ((jrVersion == null) ? ""
                                                        : "\n" + getString(R.string.version) + " " + jrVersion),
                                        EXIT);
                                return;
                            } else if (formCursor.getCount() > 1) {
                                // still take the first entry, but warn that
                                // there are multiple rows.
                                // user will need to hand-edit the SQLite
                                // database to fix it.
                                formCursor.moveToFirst();
                                formPath = formCursor
                                        .getString(formCursor.getColumnIndex(FormsColumns.FORM_FILE_PATH));
                                this.createErrorDialog(getString(R.string.survey_multiple_forms_error), EXIT);
                                return;
                            }
                        } finally {
                            if (formCursor != null) {
                                formCursor.close();
                            }
                        }
                    }
                } else if (uri != null
                        && getContentResolver().getType(uri).equals(FormsColumns.CONTENT_ITEM_TYPE)) {
                    Cursor c = null;
                    try {
                        c = getContentResolver().query(uri, null, null, null, null);
                        if (c.getCount() != 1) {
                            this.createErrorDialog(getString(R.string.bad_uri, uri), EXIT);
                            return;
                        } else {
                            c.moveToFirst();
                            formPath = c.getString(c.getColumnIndex(FormsColumns.FORM_FILE_PATH));
                            // This is the fill-blank-form code path.
                            // See if there is a savepoint for this form that
                            // has never been
                            // explicitly saved
                            // by the user. If there is, open this savepoint
                            // (resume this filled-in
                            // form).
                            // Savepoints for forms that were explicitly saved
                            // will be recovered
                            // when that
                            // explicitly saved instance is edited via
                            // edit-saved-form.
                            final String filePrefix = formPath.substring(formPath.lastIndexOf('/') + 1,
                                    formPath.lastIndexOf('.')) + "_";
                            final String fileSuffix = ".xml.save";
                            File cacheDir = new File(Collect.CACHE_PATH);
                            File[] files = cacheDir.listFiles(new FileFilter() {
                                @Override
                                public boolean accept(File pathname) {
                                    String name = pathname.getName();
                                    return name.startsWith(filePrefix) && name.endsWith(fileSuffix);
                                }
                            });
                            // see if any of these savepoints are for a
                            // filled-in form that has never been
                            // explicitly saved by the user...
                            for (int i = 0; i < files.length; ++i) {
                                File candidate = files[i];
                                String instanceDirName = candidate.getName().substring(0,
                                        candidate.getName().length() - fileSuffix.length());
                                File instanceDir = new File(
                                        Collect.INSTANCES_PATH + File.separator + instanceDirName);
                                File instanceFile = new File(instanceDir, instanceDirName + ".xml");
                                if (instanceDir.exists() && instanceDir.isDirectory() && !instanceFile.exists()) {
                                    // yes! -- use this savepoint file
                                    instancePath = instanceFile.getAbsolutePath();
                                    break;
                                }
                            }
                        }
                    } finally {
                        if (c != null) {
                            c.close();
                        }
                    }
                } else {
                    Timber.e("Unrecognized URI: %s", uri);
                    this.createErrorDialog(getString(R.string.unrecognized_uri, uri), EXIT);
                    return;
                }

                formLoaderTask = new FormLoaderTask(instancePath, null, null);
                Collect.getInstance().getActivityLogger().logAction(this, "formLoaded", formPath);
                showDialog(PROGRESS_DIALOG);
                // show dialog before we execute...
                formLoaderTask.execute(formPath);
            }
        }
    }

    private void initToolbar() {
        toolbar = (Toolbar) findViewById(R.id.toolbar);

        if (hasHardwareMenu) {
            toolbar.setTitle(getString(R.string.loading_form));
        } else {
            setTitle(getString(R.string.loading_form));
        }
        toolbar.inflateMenu(R.menu.form_menu);
        toolbar.findViewById(R.id.menu_save).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected",
                        "MENU_SAVE");
                // don't exit
                saveDataToDisk(DO_NOT_EXIT, isInstanceComplete(false), null);
            }
        });

        toolbar.findViewById(R.id.menu_goto).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                FormController formController = Collect.getInstance().getFormController();
                Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected",
                        "MENU_HIERARCHY_VIEW");
                if (formController.currentPromptIsQuestion()) {
                    saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
                }
                Intent i = new Intent(FormEntryActivity.this, FormHierarchyActivity.class);
                i.putExtra(ApplicationConstants.BundleKeys.FORM_MODE, ApplicationConstants.FormModes.EDIT_SAVED);
                startActivityForResult(i, HIERARCHY_ACTIVITY);
            }
        });
        if (!hasHardwareMenu) {
            setSupportActionBar(toolbar);
        }
    }

    /**
     * Create save-points asynchronously in order to not affect swiping performance
     * on larger forms.
     */
    private void nonblockingCreateSavePointData() {
        try {
            SavePointTask savePointTask = new SavePointTask(this);
            savePointTask.execute();
        } catch (Exception e) {
            Timber.e("Could not schedule SavePointTask. Perhaps a lot of swiping is taking place?");
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString(KEY_FORMPATH, formPath);
        FormController formController = Collect.getInstance().getFormController();
        if (formController != null) {
            outState.putString(KEY_INSTANCEPATH, formController.getInstancePath().getAbsolutePath());
            outState.putString(KEY_XPATH, formController.getXPath(formController.getFormIndex()));
            FormIndex waiting = formController.getIndexWaitingForData();
            if (waiting != null) {
                outState.putString(KEY_XPATH_WAITING_FOR_DATA, formController.getXPath(waiting));
            }
            // save the instance to a temp path...
            nonblockingCreateSavePointData();
        }
        outState.putBoolean(NEWFORM, false);
        outState.putString(KEY_ERROR, errorMessage);
        outState.putBoolean(KEY_AUTO_SAVED, autoSaved);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, final Intent intent) {
        super.onActivityResult(requestCode, resultCode, intent);
        FormController formController = Collect.getInstance().getFormController();
        if (formController == null) {
            // we must be in the midst of a reload of the FormController.
            // try to save this callback data to the FormLoaderTask
            if (formLoaderTask != null && formLoaderTask.getStatus() != AsyncTask.Status.FINISHED) {
                formLoaderTask.setActivityResult(requestCode, resultCode, intent);
            } else {
                Timber.e("Got an activityResult without any pending form loader");
            }
            return;
        }

        if (resultCode == RESULT_CANCELED) {
            // request was canceled...
            if (requestCode != HIERARCHY_ACTIVITY) {
                ((ODKView) currentView).cancelWaitingForBinaryData();
            }
            return;
        }

        // For handling results returned by the Zxing Barcode scanning library
        IntentResult barcodeScannerResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
        if (barcodeScannerResult != null) {
            if (barcodeScannerResult.getContents() == null) {
                // request was canceled...
                Timber.i("QR code scanning cancelled");
            } else {
                String sb = intent.getStringExtra("SCAN_RESULT");
                ((ODKView) currentView).setBinaryData(sb);
                saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
                refreshCurrentView();
                return;
            }
        }

        switch (requestCode) {

        case OSM_CAPTURE:
            String osmFileName = intent.getStringExtra("OSM_FILE_NAME");
            ((ODKView) currentView).setBinaryData(osmFileName);
            saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
            break;
        case EX_STRING_CAPTURE:
        case EX_INT_CAPTURE:
        case EX_DECIMAL_CAPTURE:
            String key = "value";
            boolean exists = intent.getExtras().containsKey(key);
            if (exists) {
                Object externalValue = intent.getExtras().get(key);
                ((ODKView) currentView).setBinaryData(externalValue);
                saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
            }
            break;
        case EX_GROUP_CAPTURE:
            try {
                Bundle extras = intent.getExtras();
                ((ODKView) currentView).setDataForFields(extras);
            } catch (JavaRosaException e) {
                Timber.e(e);
                createErrorDialog(e.getCause().getMessage(), DO_NOT_EXIT);
            }
            break;
        case DRAW_IMAGE:
        case ANNOTATE_IMAGE:
        case SIGNATURE_CAPTURE:
        case IMAGE_CAPTURE:
            /*
             * We saved the image to the tempfile_path, but we really want it to
             * be in: /sdcard/odk/instances/[current instnace]/something.jpg 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 fi = new File(Collect.TMPFILE_PATH);
            String instanceFolder = formController.getInstancePath().getParent();
            String s = instanceFolder + File.separator + System.currentTimeMillis() + ".jpg";

            File nf = new File(s);
            if (!fi.renameTo(nf)) {
                Timber.e("Failed to rename %s", fi.getAbsolutePath());
            } else {
                Timber.i("Renamed %s to %s", fi.getAbsolutePath(), nf.getAbsolutePath());
            }

            ((ODKView) currentView).setBinaryData(nf);
            saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
            break;
        case ALIGNED_IMAGE:
            /*
             * We saved the image to the tempfile_path; the app returns the full
             * path to the saved file in the EXTRA_OUTPUT extra. Take that file
             * and move it into the instance folder.
             */
            String path = intent.getStringExtra(android.provider.MediaStore.EXTRA_OUTPUT);
            fi = new File(path);
            instanceFolder = formController.getInstancePath().getParent();
            s = instanceFolder + File.separator + System.currentTimeMillis() + ".jpg";

            nf = new File(s);
            if (!fi.renameTo(nf)) {
                Timber.e("Failed to rename %s", fi.getAbsolutePath());
            } else {
                Timber.i("Renamed %s to %s", fi.getAbsolutePath(), nf.getAbsolutePath());
            }

            ((ODKView) currentView).setBinaryData(nf);
            saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
            break;
        case IMAGE_CHOOSER:
            /*
             * We have a saved image somewhere, but we really want it to be in:
             * /sdcard/odk/instances/[current instnace]/something.jpg 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
             */

            showDialog(SAVING_IMAGE_DIALOG);
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    saveChosenImage(intent.getData());
                }
            };
            new Thread(runnable).start();

            break;
        case AUDIO_CAPTURE:
        case VIDEO_CAPTURE:
            Uri mediaUri = intent.getData();
            saveAudioVideoAnswer(mediaUri);
            String filePath = MediaUtils.getDataColumn(this, mediaUri, null, null);
            if (filePath != null) {
                new File(filePath).delete();
            }
            try {
                getContentResolver().delete(mediaUri, null, null);
            } catch (Exception e) {
                Timber.e(e);
            }
            break;

        case AUDIO_CHOOSER:
        case VIDEO_CHOOSER:
            saveAudioVideoAnswer(intent.getData());
            break;
        case LOCATION_CAPTURE:
            String sl = intent.getStringExtra(LOCATION_RESULT);
            ((ODKView) currentView).setBinaryData(sl);
            saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
            break;
        case GEOSHAPE_CAPTURE:
            //String ls = intent.getStringExtra(GEOSHAPE_RESULTS);
            String gshr = intent.getStringExtra(GEOSHAPE_RESULTS);
            ((ODKView) currentView).setBinaryData(gshr);
            saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
            break;
        case GEOTRACE_CAPTURE:
            String traceExtra = intent.getStringExtra(GEOTRACE_RESULTS);
            ((ODKView) currentView).setBinaryData(traceExtra);
            saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
            break;
        case BEARING_CAPTURE:
            String bearing = intent.getStringExtra(BEARING_RESULT);
            ((ODKView) currentView).setBinaryData(bearing);
            saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
            break;
        case HIERARCHY_ACTIVITY:
            // We may have jumped to a new index in hierarchy activity, so
            // refresh
            break;

        }
        refreshCurrentView();
    }

    private void saveChosenImage(Uri selectedImage) {
        // Copy file to sdcard
        String instanceFolder1 = Collect.getInstance().getFormController().getInstancePath().getParent();
        String destImagePath = instanceFolder1 + File.separator + System.currentTimeMillis() + ".jpg";

        File chosenImage;
        try {
            chosenImage = MediaUtils.getFileFromUri(this, selectedImage, Images.Media.DATA);
            if (chosenImage != null) {
                final File newImage = new File(destImagePath);
                FileUtils.copyFile(chosenImage, newImage);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        dismissDialog(SAVING_IMAGE_DIALOG);
                        ((ODKView) currentView).setBinaryData(newImage);
                        saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
                        refreshCurrentView();
                    }
                });
            } else {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        dismissDialog(SAVING_IMAGE_DIALOG);
                        Timber.e("Could not receive chosen image");
                        showCustomToast(getString(R.string.error_occured), Toast.LENGTH_SHORT);
                    }
                });
            }
        } catch (GDriveConnectionException e) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    dismissDialog(SAVING_IMAGE_DIALOG);
                    Timber.e("Could not receive chosen image due to connection problem");
                    showCustomToast(getString(R.string.gdrive_connection_exception), Toast.LENGTH_LONG);
                }
            });
        }
    }

    private void saveAudioVideoAnswer(Uri media) {
        // 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.
        ((ODKView) currentView).setBinaryData(media);
        saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
    }

    /**
     * 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.
     */
    public void refreshCurrentView() {
        FormController formController = Collect.getInstance().getFormController();
        int event = formController.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.

        View current = createView(event, false);
        showView(current, AnimationType.FADE);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateOptionsMenu", "show");
        super.onCreateOptionsMenu(menu);

        if (!hasHardwareMenu) {
            menu.add(0, MENU_SAVE, 0, R.string.save_all_answers).setIcon(android.R.drawable.ic_menu_save)
                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);

            menu.add(0, MENU_HIERARCHY_VIEW, 0, R.string.view_hierarchy).setIcon(R.drawable.ic_menu_goto)
                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
        }

        menu.add(0, MENU_LANGUAGES, 0, R.string.change_language).setIcon(R.drawable.ic_menu_start_conversation)
                .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);

        menu.add(0, MENU_PREFERENCES, 0, R.string.general_preferences).setIcon(R.drawable.ic_menu_preferences)
                .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);

        if (adminPreferences == null) {
            return false;
        }

        boolean useability;

        if (!hasHardwareMenu) {
            useability = adminPreferences.getBoolean(AdminKeys.KEY_SAVE_MID, true);

            menu.findItem(MENU_SAVE).setVisible(useability).setEnabled(useability);

            useability = adminPreferences.getBoolean(AdminKeys.KEY_JUMP_TO, true);

            menu.findItem(MENU_HIERARCHY_VIEW).setVisible(useability).setEnabled(useability);
        }

        FormController formController = Collect.getInstance().getFormController();

        useability = adminPreferences.getBoolean(AdminKeys.KEY_CHANGE_LANGUAGE, true) && (formController != null)
                && formController.getLanguages() != null && formController.getLanguages().length > 1;

        menu.findItem(MENU_LANGUAGES).setVisible(useability).setEnabled(useability);

        useability = adminPreferences.getBoolean(AdminKeys.KEY_ACCESS_SETTINGS, true);

        menu.findItem(MENU_PREFERENCES).setVisible(useability).setEnabled(useability);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        FormController formController = Collect.getInstance().getFormController();
        switch (item.getItemId()) {
        case MENU_LANGUAGES:
            Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected",
                    "MENU_LANGUAGES");
            createLanguageDialog();
            return true;
        case MENU_SAVE:
            Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected", "MENU_SAVE");
            // don't exit
            saveDataToDisk(DO_NOT_EXIT, isInstanceComplete(false), null);
            return true;
        case MENU_HIERARCHY_VIEW:
            Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected",
                    "MENU_HIERARCHY_VIEW");
            if (formController.currentPromptIsQuestion()) {
                saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
            }
            Intent i = new Intent(this, FormHierarchyActivity.class);
            i.putExtra(ApplicationConstants.BundleKeys.FORM_MODE, ApplicationConstants.FormModes.EDIT_SAVED);
            startActivityForResult(i, HIERARCHY_ACTIVITY);
            return true;
        case MENU_PREFERENCES:
            Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected",
                    "MENU_PREFERENCES");
            Intent pref = new Intent(this, PreferencesActivity.class);
            startActivity(pref);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    /**
     * Attempt to save the answer(s) in the current screen to into the data
     * model.
     *
     * @return false if any error occurs while saving (constraint violated,
     * etc...), true otherwise.
     */
    private boolean saveAnswersForCurrentScreen(boolean evaluateConstraints) {
        FormController formController = Collect.getInstance().getFormController();
        // only try to save if the current event is a question or a field-list group
        // and current view is an ODKView (occasionally we show blank views that do not have any
        // controls to save data from)
        if (formController.currentPromptIsQuestion() && currentView instanceof ODKView) {
            LinkedHashMap<FormIndex, IAnswerData> answers = ((ODKView) currentView).getAnswers();
            try {
                FailedConstraint constraint = formController.saveAllScreenAnswers(answers, evaluateConstraints);
                if (constraint != null) {
                    createConstraintToast(constraint.index, constraint.status);
                    return false;
                }
            } catch (JavaRosaException e) {
                Timber.e(e);
                createErrorDialog(e.getCause().getMessage(), DO_NOT_EXIT);
                return false;
            }
        }
        return true;
    }

    /**
     * Clears the answer on the screen.
     */
    private void clearAnswer(QuestionWidget qw) {
        if (qw.getAnswer() != null) {
            qw.clearAnswer();
        }
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);
        Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateContextMenu", "show");
        FormController formController = Collect.getInstance().getFormController();

        menu.add(0, v.getId(), 0, getString(R.string.clear_answer));
        if (formController.indexContainsRepeatableGroup()) {
            menu.add(0, DELETE_REPEAT, 0, getString(R.string.delete_repeat));
        }
        menu.setHeaderTitle(getString(R.string.edit_prompt));
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        if (item.getItemId() == DELETE_REPEAT) {
            Collect.getInstance().getActivityLogger().logInstanceAction(this, "onContextItemSelected",
                    "createDeleteRepeatConfirmDialog");
            createDeleteRepeatConfirmDialog();
        } else {
            /*
            * 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.
            */
            boolean shouldClearDialogBeShown;
            for (QuestionWidget qw : ((ODKView) currentView).getWidgets()) {
                shouldClearDialogBeShown = false;
                if (qw instanceof StringWidget) {
                    for (int i = 0; i < qw.getChildCount(); i++) {
                        if (item.getItemId() == qw.getChildAt(i).getId()) {
                            shouldClearDialogBeShown = true;
                            break;
                        }
                    }
                } else if (item.getItemId() == qw.getId()) {
                    shouldClearDialogBeShown = true;
                }

                if (shouldClearDialogBeShown) {
                    Collect.getInstance().getActivityLogger().logInstanceAction(this, "onContextItemSelected",
                            "createClearDialog", qw.getPrompt().getIndex());
                    createClearDialog(qw);
                    break;
                }
            }
        }

        return super.onContextItemSelected(item);
    }

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

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

        // mFormEntryController is static so we don't need to pass it.
        if (formController != null && formController.currentPromptIsQuestion()) {
            saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
        }
        return null;
    }

    /**
     * Creates a view given the View type and an event
     *
     * @param advancingPage -- true if this results from advancing through the form
     * @return newly created View
     */
    private View createView(int event, boolean advancingPage) {
        FormController formController = Collect.getInstance().getFormController();

        if (hasHardwareMenu) {
            toolbar.setTitle(formController.getFormTitle());
        } else {
            setTitle(formController.getFormTitle());
        }

        switch (event) {
        case FormEntryController.EVENT_BEGINNING_OF_FORM:
            return createViewForFormBeginning(event, true, formController);

        case FormEntryController.EVENT_END_OF_FORM:
            View endView = View.inflate(this, R.layout.form_entry_end, null);
            ((TextView) endView.findViewById(R.id.description))
                    .setText(getString(R.string.save_enter_data_description, formController.getFormTitle()));

            // checkbox for if finished or ready to send
            final CheckBox instanceComplete = ((CheckBox) endView.findViewById(R.id.mark_finished));
            instanceComplete.setChecked(isInstanceComplete(true));

            if (!adminPreferences.getBoolean(AdminKeys.KEY_MARK_AS_FINALIZED, true)) {
                instanceComplete.setVisibility(View.GONE);
            }

            // edittext to change the displayed name of the instance
            final EditText saveAs = (EditText) endView.findViewById(R.id.save_name);

            // disallow carriage returns in the name
            InputFilter returnFilter = new InputFilter() {
                public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
                        int dend) {
                    for (int i = start; i < end; i++) {
                        if (Character.getType((source.charAt(i))) == Character.CONTROL) {
                            return "";
                        }
                    }
                    return null;
                }
            };
            saveAs.setFilters(new InputFilter[] { returnFilter });

            String saveName = formController.getSubmissionMetadata().instanceName;
            if (saveName == null) {
                // no meta/instanceName field in the form -- see if we have a
                // name for this instance from a previous save attempt...
                if (getContentResolver().getType(getIntent().getData()) == InstanceColumns.CONTENT_ITEM_TYPE) {
                    Uri instanceUri = getIntent().getData();
                    Cursor instance = null;
                    try {
                        instance = getContentResolver().query(instanceUri, null, null, null, null);
                        if (instance.getCount() == 1) {
                            instance.moveToFirst();
                            saveName = instance.getString(instance.getColumnIndex(InstanceColumns.DISPLAY_NAME));
                        }
                    } finally {
                        if (instance != null) {
                            instance.close();
                        }
                    }
                }
                if (saveName == null) {
                    // last resort, default to the form title
                    saveName = formController.getFormTitle();
                }
                // present the prompt to allow user to name the form
                TextView sa = (TextView) endView.findViewById(R.id.save_form_as);
                sa.setVisibility(View.VISIBLE);
                saveAs.setText(saveName);
                saveAs.setEnabled(true);
                saveAs.setVisibility(View.VISIBLE);
            } else {
                // if instanceName is defined in form, this is the name -- no
                // revisions
                // display only the name, not the prompt, and disable edits
                TextView sa = (TextView) endView.findViewById(R.id.save_form_as);
                sa.setVisibility(View.GONE);
                saveAs.setText(saveName);
                saveAs.setEnabled(false);
                saveAs.setVisibility(View.VISIBLE);
            }

            // override the visibility settings based upon admin preferences
            if (!adminPreferences.getBoolean(AdminKeys.KEY_SAVE_AS, true)) {
                saveAs.setVisibility(View.GONE);
                TextView sa = (TextView) endView.findViewById(R.id.save_form_as);
                sa.setVisibility(View.GONE);
            }

            // Create 'save' button
            endView.findViewById(R.id.save_exit_button).setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    Collect.getInstance().getActivityLogger().logInstanceAction(this, "createView.saveAndExit",
                            instanceComplete.isChecked() ? "saveAsComplete" : "saveIncomplete");
                    // Form is marked as 'saved' here.
                    if (saveAs.getText().length() < 1) {
                        ToastUtils.showShortToast(R.string.save_as_error);
                    } else {
                        saveDataToDisk(EXIT, instanceComplete.isChecked(), saveAs.getText().toString());
                    }
                }
            });

            if (showNavigationButtons) {
                backButton.setEnabled(true);
                nextButton.setEnabled(false);
            }

            return endView;
        case FormEntryController.EVENT_QUESTION:
        case FormEntryController.EVENT_GROUP:
        case FormEntryController.EVENT_REPEAT:
            ODKView odkv = null;
            // should only be a group here if the event_group is a field-list
            try {
                FormEntryPrompt[] prompts = formController.getQuestionPrompts();
                FormEntryCaption[] groups = formController.getGroupsForCurrentIndex();
                odkv = new ODKView(this, formController.getQuestionPrompts(), groups, advancingPage);
                Timber.i("Created view for group %s %s",
                        (groups.length > 0 ? groups[groups.length - 1].getLongText() : "[top]"),
                        (prompts.length > 0 ? prompts[0].getQuestionText() : "[no question]"));
            } catch (RuntimeException e) {
                Timber.e(e);
                // this is badness to avoid a crash.
                try {
                    event = formController.stepToNextScreenEvent();
                    createErrorDialog(e.getMessage(), DO_NOT_EXIT);
                } catch (JavaRosaException e1) {
                    Timber.e(e1);
                    createErrorDialog(e.getMessage() + "\n\n" + e1.getCause().getMessage(), DO_NOT_EXIT);
                }
                return createView(event, advancingPage);
            }

            // Makes a "clear answer" menu pop up on long-click
            for (QuestionWidget qw : odkv.getWidgets()) {
                if (!qw.getPrompt().isReadOnly()) {
                    // If it's a StringWidget register all its elements apart from EditText as
                    // we want to enable paste option after long click on the EditText
                    if (qw instanceof StringWidget) {
                        for (int i = 0; i < qw.getChildCount(); i++) {
                            if (!(qw.getChildAt(i) instanceof EditText)) {
                                registerForContextMenu(qw.getChildAt(i));
                            }
                        }
                    } else {
                        registerForContextMenu(qw);
                    }
                }
            }

            if (showNavigationButtons) {
                adjustBackNavigationButtonVisibility();
                nextButton.setEnabled(true);
            }
            return odkv;

        case FormEntryController.EVENT_PROMPT_NEW_REPEAT:
            createRepeatDialog();
            return new EmptyView(this);

        default:
            Timber.e("Attempted to create a view that does not exist.");
            // this is badness to avoid a crash.
            try {
                event = formController.stepToNextScreenEvent();
                createErrorDialog(getString(R.string.survey_internal_error), EXIT);
            } catch (JavaRosaException e) {
                Timber.e(e);
                createErrorDialog(e.getCause().getMessage(), EXIT);
            }
            return createView(event, advancingPage);
        }
    }

    /**
     * Disables the back button if it is first question....
     */
    private void adjustBackNavigationButtonVisibility() {
        FormController formController = Collect.getInstance().getFormController();
        try {
            boolean firstQuestion = formController
                    .stepToPreviousScreenEvent() == FormEntryController.EVENT_BEGINNING_OF_FORM;
            backButton.setEnabled(!firstQuestion);
            formController.stepToNextScreenEvent();
            if (formController.getEvent() == FormEntryController.EVENT_PROMPT_NEW_REPEAT) {
                backButton.setEnabled(true);
                formController.stepToNextScreenEvent();
            }
        } catch (JavaRosaException e) {
            backButton.setEnabled(true);
            Timber.e(e);
        }
    }

    private View createViewForFormBeginning(int event, boolean advancingPage, FormController formController) {
        try {
            event = formController.stepToNextScreenEvent();

        } catch (JavaRosaException e) {
            Timber.e(e);
            createErrorDialog(e.getMessage() + "\n\n" + e.getCause().getMessage(), DO_NOT_EXIT);
        }

        return createView(event, advancingPage);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent mv) {
        boolean handled = gestureDetector.onTouchEvent(mv);
        if (!handled) {
            return super.dispatchTouchEvent(mv);
        }

        return handled; // this is always true
    }

    /**
     * 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() {
        try {
            FormController formController = Collect.getInstance().getFormController();

            // get constraint behavior preference value with appropriate default
            String constraintBehavior = PreferenceManager.getDefaultSharedPreferences(this)
                    .getString(PreferenceKeys.KEY_CONSTRAINT_BEHAVIOR, PreferenceKeys.CONSTRAINT_BEHAVIOR_DEFAULT);

            if (formController.currentPromptIsQuestion()) {

                // if constraint behavior says we should validate on swipe, do so
                if (constraintBehavior.equals(PreferenceKeys.CONSTRAINT_BEHAVIOR_ON_SWIPE)) {
                    if (!saveAnswersForCurrentScreen(EVALUATE_CONSTRAINTS)) {
                        // A constraint was violated so a dialog should be showing.
                        beenSwiped = false;
                        return;
                    }

                    // otherwise, just save without validating (constraints will be validated on
                    // finalize)
                } else {
                    saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
                }
            }

            View next;

            int originalEvent = formController.getEvent();
            int event = formController.stepToNextScreenEvent();

            // Helps prevent transition animation at the end of the form (if user swipes left
            // she will stay on the same screen)
            if (originalEvent == event && originalEvent == FormEntryController.EVENT_END_OF_FORM) {
                beenSwiped = false;
                return;
            }

            switch (event) {
            case FormEntryController.EVENT_QUESTION:
            case FormEntryController.EVENT_GROUP:
                // create a savepoint
                if ((++viewCount) % SAVEPOINT_INTERVAL == 0) {
                    nonblockingCreateSavePointData();
                }
                next = createView(event, true);
                showView(next, AnimationType.RIGHT);
                break;
            case FormEntryController.EVENT_END_OF_FORM:
            case FormEntryController.EVENT_REPEAT:
            case FormEntryController.EVENT_PROMPT_NEW_REPEAT:
                next = createView(event, true);
                showView(next, AnimationType.RIGHT);
                break;
            case FormEntryController.EVENT_REPEAT_JUNCTURE:
                Timber.i("Repeat juncture: %s", formController.getFormIndex().getReference());
                // skip repeat junctures until we implement them
                break;
            default:
                Timber.w("JavaRosa added a new EVENT type and didn't tell us... shame on them.");
                break;
            }
        } catch (JavaRosaException e) {
            Timber.e(e);
            createErrorDialog(e.getCause().getMessage(), DO_NOT_EXIT);
        }
    }

    /**
     * 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.
     */
    private void showPreviousView() {
        try {
            FormController formController = Collect.getInstance().getFormController();
            // The answer is saved on a back swipe, but question constraints are
            // ignored.
            if (formController.currentPromptIsQuestion()) {
                saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
            }

            if (formController.getEvent() != FormEntryController.EVENT_BEGINNING_OF_FORM) {
                int event = formController.stepToPreviousScreenEvent();

                // If we are the begining of the form, lets revert our actions and ignore
                // this swipe
                if (event == FormEntryController.EVENT_BEGINNING_OF_FORM) {
                    event = formController.stepToNextScreenEvent();
                    beenSwiped = false;

                    if (event != FormEntryController.EVENT_PROMPT_NEW_REPEAT) {
                        // Returning here prevents the same view sliding when user is on the first screen
                        return;
                    }
                }

                if (event == FormEntryController.EVENT_GROUP || event == FormEntryController.EVENT_QUESTION) {
                    // create savepoint
                    if ((++viewCount) % SAVEPOINT_INTERVAL == 0) {
                        nonblockingCreateSavePointData();
                    }
                }
                View next = createView(event, false);
                showView(next, AnimationType.LEFT);
            } else {
                beenSwiped = false;
            }
        } catch (JavaRosaException e) {
            Timber.e(e);
            createErrorDialog(e.getCause().getMessage(), DO_NOT_EXIT);
        }
    }

    /**
     * Displays the View specified by the parameter 'next', animating both the
     * current view and next appropriately given the AnimationType. Also updates
     * the progress bar.
     */
    public void showView(View next, AnimationType from) {

        // disable notifications...
        if (inAnimation != null) {
            inAnimation.setAnimationListener(null);
        }
        if (outAnimation != null) {
            outAnimation.setAnimationListener(null);
        }

        // logging of the view being shown is already done, as this was handled
        // by createView()
        switch (from) {
        case RIGHT:
            inAnimation = AnimationUtils.loadAnimation(this, R.anim.push_left_in);
            outAnimation = AnimationUtils.loadAnimation(this, R.anim.push_left_out);
            // if animation is left or right then it was a swipe, and we want to re-save on
            // entry
            autoSaved = false;
            break;
        case LEFT:
            inAnimation = AnimationUtils.loadAnimation(this, R.anim.push_right_in);
            outAnimation = AnimationUtils.loadAnimation(this, R.anim.push_right_out);
            autoSaved = false;
            break;
        case FADE:
            inAnimation = AnimationUtils.loadAnimation(this, R.anim.fade_in);
            outAnimation = AnimationUtils.loadAnimation(this, R.anim.fade_out);
            break;
        }

        // complete setup for animations...
        inAnimation.setAnimationListener(this);
        outAnimation.setAnimationListener(this);

        // drop keyboard before transition...
        if (currentView != null) {
            InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
            inputManager.hideSoftInputFromWindow(currentView.getWindowToken(), 0);
        }

        RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);

        // adjust which view is in the layout container...
        staleView = currentView;
        currentView = next;
        questionHolder.addView(currentView, lp);
        animationCompletionSet = 0;

        if (staleView != null) {
            // start OutAnimation for transition...
            staleView.startAnimation(outAnimation);
            // and remove the old view (MUST occur after start of animation!!!)
            questionHolder.removeView(staleView);
        } else {
            animationCompletionSet = 2;
        }
        // start InAnimation for transition...
        currentView.startAnimation(inAnimation);

        String logString = "";
        switch (from) {
        case RIGHT:
            logString = "next";
            break;
        case LEFT:
            logString = "previous";
            break;
        case FADE:
            logString = "refresh";
            break;
        }

        Collect.getInstance().getActivityLogger().logInstanceAction(this, "showView", logString);

        FormController formController = Collect.getInstance().getFormController();
        if (formController.getEvent() == FormEntryController.EVENT_QUESTION
                || formController.getEvent() == FormEntryController.EVENT_GROUP
                || formController.getEvent() == FormEntryController.EVENT_REPEAT) {
            FormEntryPrompt[] prompts = Collect.getInstance().getFormController().getQuestionPrompts();
            for (FormEntryPrompt p : prompts) {
                List<TreeElement> attrs = p.getBindAttributes();
                for (int i = 0; i < attrs.size(); i++) {
                    if (!autoSaved && "saveIncomplete".equals(attrs.get(i).getName())) {
                        saveDataToDisk(false, false, null, false);
                        autoSaved = true;
                    }
                }
            }
        }
    }

    // Hopefully someday we can use managed dialogs when the bugs are fixed
    /*
     * Ideally, we'd like to use Android to manage dialogs with onCreateDialog()
     * and onPrepareDialog(), but dialogs with dynamic content are broken in 1.5
     * (cupcake). We do use managed dialogs for our static loading
     * ProgressDialog. The main issue we noticed and are waiting to see fixed
     * is: onPrepareDialog() is not called after a screen orientation change.
     * http://code.google.com/p/android/issues/detail?id=1639
     */

    //

    /**
     * Creates and displays a dialog displaying the violated constraint.
     */
    private void createConstraintToast(FormIndex index, int saveStatus) {
        FormController formController = Collect.getInstance().getFormController();
        String constraintText;
        switch (saveStatus) {
        case FormEntryController.ANSWER_CONSTRAINT_VIOLATED:
            Collect.getInstance().getActivityLogger().logInstanceAction(this,
                    "createConstraintToast.ANSWER_CONSTRAINT_VIOLATED", "show", index);
            constraintText = formController.getQuestionPromptConstraintText(index);
            if (constraintText == null) {
                constraintText = formController.getQuestionPrompt(index)
                        .getSpecialFormQuestionText("constraintMsg");
                if (constraintText == null) {
                    constraintText = getString(R.string.invalid_answer_error);
                }
            }
            break;
        case FormEntryController.ANSWER_REQUIRED_BUT_EMPTY:
            Collect.getInstance().getActivityLogger().logInstanceAction(this,
                    "createConstraintToast.ANSWER_REQUIRED_BUT_EMPTY", "show", index);
            constraintText = formController.getQuestionPromptRequiredText(index);
            if (constraintText == null) {
                constraintText = formController.getQuestionPrompt(index).getSpecialFormQuestionText("requiredMsg");
                if (constraintText == null) {
                    constraintText = getString(R.string.required_answer_error);
                }
            }
            break;
        default:
            return;
        }

        showCustomToast(constraintText, Toast.LENGTH_SHORT);
    }

    /**
     * Creates a toast with the specified message.
     */
    private void showCustomToast(String message, int duration) {
        LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        View view = inflater.inflate(R.layout.toast_view, null);

        // set the text in the view
        TextView tv = (TextView) view.findViewById(R.id.message);
        tv.setText(message);

        Toast t = new Toast(this);
        t.setView(view);
        t.setDuration(duration);
        t.setGravity(Gravity.CENTER, 0, 0);
        t.show();
    }

    /**
     * Creates and displays a dialog asking the user if they'd like to create a
     * repeat of the current group.
     */
    private void createRepeatDialog() {
        Collect.getInstance().getActivityLogger().logInstanceAction(this, "createRepeatDialog", "show");

        // In some cases dialog might be present twice because refreshView() is being called
        // from onResume(). This ensures that we do not preset this modal dialog if it's already
        // visible. Checking for shownAlertDialogIsGroupRepeat because the same field
        // alertDialog is being used for all alert dialogs in this activity.
        if (alertDialog != null && alertDialog.isShowing() && shownAlertDialogIsGroupRepeat) {
            return;
        }

        alertDialog = new AlertDialog.Builder(this).create();
        alertDialog.setIcon(android.R.drawable.ic_dialog_info);
        DialogInterface.OnClickListener repeatListener = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int i) {
                shownAlertDialogIsGroupRepeat = false;
                FormController formController = Collect.getInstance().getFormController();
                switch (i) {
                case DialogInterface.BUTTON_POSITIVE: // yes, repeat
                    Collect.getInstance().getActivityLogger().logInstanceAction(this, "createRepeatDialog",
                            "addRepeat");
                    try {
                        formController.newRepeat();
                    } catch (Exception e) {
                        FormEntryActivity.this.createErrorDialog(e.getMessage(), DO_NOT_EXIT);
                        return;
                    }
                    if (!formController.indexIsInFieldList()) {
                        // we are at a REPEAT event that does not have a
                        // field-list appearance
                        // step to the next visible field...
                        // which could be the start of a new repeat group...
                        showNextView();
                    } else {
                        // we are at a REPEAT event that has a field-list
                        // appearance
                        // just display this REPEAT event's group.
                        refreshCurrentView();
                    }
                    break;
                case DialogInterface.BUTTON_NEGATIVE: // no, no repeat
                    Collect.getInstance().getActivityLogger().logInstanceAction(this, "createRepeatDialog",
                            "showNext");

                    //
                    // Make sure the error dialog will not disappear.
                    //
                    // When showNextView() popups an error dialog (because of a
                    // JavaRosaException)
                    // the issue is that the "add new repeat dialog" is referenced by
                    // alertDialog
                    // like the error dialog. When the "no repeat" is clicked, the error dialog
                    // is shown. Android by default dismisses the dialogs when a button is
                    // clicked,
                    // so instead of closing the first dialog, it closes the second.
                    new Thread() {

                        @Override
                        public void run() {
                            FormEntryActivity.this.runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    try {
                                        Thread.sleep(500);
                                    } catch (InterruptedException e) {
                                        //This is rare
                                        Timber.e(e);
                                    }
                                    showNextView();
                                }
                            });
                        }
                    }.start();

                    break;
                }
            }
        };
        FormController formController = Collect.getInstance().getFormController();
        if (formController.getLastRepeatCount() > 0) {
            alertDialog.setTitle(getString(R.string.leaving_repeat_ask));
            alertDialog.setMessage(getString(R.string.add_another_repeat, formController.getLastGroupText()));
            alertDialog.setButton(getString(R.string.add_another), repeatListener);
            alertDialog.setButton2(getString(R.string.leave_repeat_yes), repeatListener);

        } else {
            alertDialog.setTitle(getString(R.string.entering_repeat_ask));
            alertDialog.setMessage(getString(R.string.add_repeat, formController.getLastGroupText()));
            alertDialog.setButton(getString(R.string.entering_repeat), repeatListener);
            alertDialog.setButton2(getString(R.string.add_repeat_no), repeatListener);
        }
        alertDialog.setCancelable(false);
        beenSwiped = false;
        shownAlertDialogIsGroupRepeat = true;
        alertDialog.show();
    }

    /**
     * Creates and displays dialog with the given errorMsg.
     */
    private void createErrorDialog(String errorMsg, final boolean shouldExit) {
        Collect.getInstance().getActivityLogger().logInstanceAction(this, "createErrorDialog",
                "show." + Boolean.toString(shouldExit));

        if (alertDialog != null && alertDialog.isShowing()) {
            errorMsg = errorMessage + "\n\n" + errorMsg;
            errorMessage = errorMsg;
        } else {
            alertDialog = new AlertDialog.Builder(this).create();
            errorMessage = errorMsg;
        }

        alertDialog.setIcon(android.R.drawable.ic_dialog_info);
        alertDialog.setTitle(getString(R.string.error_occured));
        alertDialog.setMessage(errorMsg);
        DialogInterface.OnClickListener errorListener = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int i) {
                switch (i) {
                case DialogInterface.BUTTON_POSITIVE:
                    Collect.getInstance().getActivityLogger().logInstanceAction(this, "createErrorDialog", "OK");
                    if (shouldExit) {
                        errorMessage = null;
                        finish();
                    }
                    break;
                }
            }
        };
        alertDialog.setCancelable(false);
        alertDialog.setButton(getString(R.string.ok), errorListener);
        beenSwiped = false;
        alertDialog.show();
    }

    /**
     * Creates a confirm/cancel dialog for deleting repeats.
     */
    private void createDeleteRepeatConfirmDialog() {
        Collect.getInstance().getActivityLogger().logInstanceAction(this, "createDeleteRepeatConfirmDialog",
                "show");
        FormController formController = Collect.getInstance().getFormController();

        alertDialog = new AlertDialog.Builder(this).create();
        alertDialog.setIcon(android.R.drawable.ic_dialog_info);
        String name = formController.getLastRepeatedGroupName();
        int repeatcount = formController.getLastRepeatedGroupRepeatCount();
        if (repeatcount != -1) {
            name += " (" + (repeatcount + 1) + ")";
        }
        alertDialog.setTitle(getString(R.string.delete_repeat_ask));
        alertDialog.setMessage(getString(R.string.delete_repeat_confirm, name));
        DialogInterface.OnClickListener quitListener = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int i) {
                FormController formController = Collect.getInstance().getFormController();
                switch (i) {
                case DialogInterface.BUTTON_POSITIVE: // yes
                    Collect.getInstance().getActivityLogger().logInstanceAction(this,
                            "createDeleteRepeatConfirmDialog", "OK");
                    formController.deleteRepeat();
                    showNextView();
                    break;

                case DialogInterface.BUTTON_NEGATIVE: // no
                    Collect.getInstance().getActivityLogger().logInstanceAction(this,
                            "createDeleteRepeatConfirmDialog", "cancel");

                    refreshCurrentView();
                    break;
                }
            }
        };
        alertDialog.setCancelable(false);
        alertDialog.setButton(getString(R.string.discard_group), quitListener);
        alertDialog.setButton2(getString(R.string.delete_repeat_no), quitListener);
        alertDialog.show();
    }

    /**
     * Saves data and writes it to disk. If exit is set, program will exit after
     * save completes. Complete indicates whether the user has marked the
     * isntancs as complete. If updatedSaveName is non-null, the instances
     * content provider is updated with the new name
     */
    // by default, save the current screen
    private boolean saveDataToDisk(boolean exit, boolean complete, String updatedSaveName) {
        return saveDataToDisk(exit, complete, updatedSaveName, true);
    }

    // but if you want save in the background, can't be current screen
    private boolean saveDataToDisk(boolean exit, boolean complete, String updatedSaveName, boolean current) {
        // save current answer
        if (current) {
            if (!saveAnswersForCurrentScreen(complete)) {
                ToastUtils.showShortToast(R.string.data_saved_error);
                return false;
            }
        }

        synchronized (saveDialogLock) {
            saveToDiskTask = new SaveToDiskTask(getIntent().getData(), exit, complete, updatedSaveName);
            saveToDiskTask.setFormSavedListener(this);
            autoSaved = true;
            showDialog(SAVING_DIALOG);
            // show dialog before we execute...
            saveToDiskTask.execute();
        }

        return true;
    }

    /**
     * Create a dialog with options to save and exit, save, or quit without
     * saving
     */
    private void createQuitDialog() {
        String title;
        {
            FormController formController = Collect.getInstance().getFormController();
            title = (formController == null) ? null : formController.getFormTitle();
            if (title == null) {
                title = getString(R.string.no_form_loaded);
            }
        }

        String[] items;
        if (adminPreferences.getBoolean(AdminKeys.KEY_SAVE_MID, true)) {
            items = new String[] { getString(R.string.keep_changes), getString(R.string.do_not_save) };
        } else {
            items = new String[] { getString(R.string.do_not_save) };
        }

        Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog", "show");
        alertDialog = new AlertDialog.Builder(this).setIcon(android.R.drawable.ic_dialog_info)
                .setTitle(getString(R.string.quit_application, title))
                .setNeutralButton(getString(R.string.do_not_exit), new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int id) {

                        Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog",
                                "cancel");
                        dialog.cancel();

                    }
                }).setItems(items, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        switch (which) {

                        case 0: // save and exit
                            // this is slightly complicated because if the
                            // option is disabled in
                            // the admin menu, then case 0 actually becomes
                            // 'discard and exit'
                            // whereas if it's enabled it's 'save and exit'
                            if (adminPreferences.getBoolean(AdminKeys.KEY_SAVE_MID, true)) {
                                Collect.getInstance().getActivityLogger().logInstanceAction(this,
                                        "createQuitDialog", "saveAndExit");
                                saveDataToDisk(EXIT, isInstanceComplete(false), null);
                            } else {
                                Collect.getInstance().getActivityLogger().logInstanceAction(this,
                                        "createQuitDialog", "discardAndExit");
                                removeTempInstance();
                                finishReturnInstance();
                            }
                            break;

                        case 1: // discard changes and exit
                            Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog",
                                    "discardAndExit");

                            // close all open databases of external data.
                            Collect.getInstance().getExternalDataManager().close();

                            removeTempInstance();
                            finishReturnInstance();
                            break;

                        case 2:// do nothing
                            Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog",
                                    "cancel");
                            break;
                        }
                    }
                }).create();
        alertDialog.show();
    }

    /**
     * this method cleans up unneeded files when the user selects 'discard and
     * exit'
     */
    private void removeTempInstance() {
        FormController formController = Collect.getInstance().getFormController();

        // attempt to remove any scratch file
        File temp = SaveToDiskTask.savepointFile(formController.getInstancePath());
        if (temp.exists()) {
            temp.delete();
        }

        boolean erase = false;
        {
            Cursor c = null;
            try {
                c = new InstancesDao()
                        .getInstancesCursorForFilePath(formController.getInstancePath().getAbsolutePath());
                erase = (c.getCount() < 1);
            } finally {
                if (c != null) {
                    c.close();
                }
            }
        }

        // if it's not already saved, erase everything
        if (erase) {
            // delete media first
            String instanceFolder = formController.getInstancePath().getParent();
            Timber.i("Attempting to delete: %s", instanceFolder);
            int images = MediaUtils
                    .deleteImagesInFolderFromMediaProvider(formController.getInstancePath().getParentFile());
            int audio = MediaUtils
                    .deleteAudioInFolderFromMediaProvider(formController.getInstancePath().getParentFile());
            int video = MediaUtils
                    .deleteVideoInFolderFromMediaProvider(formController.getInstancePath().getParentFile());

            Timber.i("Removed from content providers: %d image files, %d audio files and %d audio files.", images,
                    audio, video);
            File f = new File(instanceFolder);
            if (f.exists() && f.isDirectory()) {
                for (File del : f.listFiles()) {
                    Timber.i("Deleting file: %s", del.getAbsolutePath());
                    del.delete();
                }
                f.delete();
            }
        }
    }

    /**
     * Confirm clear answer dialog
     */
    private void createClearDialog(final QuestionWidget qw) {
        Collect.getInstance().getActivityLogger().logInstanceAction(this, "createClearDialog", "show",
                qw.getPrompt().getIndex());
        alertDialog = new AlertDialog.Builder(this).create();
        alertDialog.setIcon(android.R.drawable.ic_dialog_info);

        alertDialog.setTitle(getString(R.string.clear_answer_ask));

        String question = qw.getPrompt().getLongText();
        if (question == null) {
            question = "";
        }
        if (question.length() > 50) {
            question = question.substring(0, 50) + "...";
        }

        alertDialog.setMessage(getString(R.string.clearanswer_confirm, question));

        DialogInterface.OnClickListener quitListener = new DialogInterface.OnClickListener() {

            @Override
            public void onClick(DialogInterface dialog, int i) {
                switch (i) {
                case DialogInterface.BUTTON_POSITIVE: // yes
                    Collect.getInstance().getActivityLogger().logInstanceAction(this, "createClearDialog",
                            "clearAnswer", qw.getPrompt().getIndex());
                    clearAnswer(qw);
                    saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
                    break;
                case DialogInterface.BUTTON_NEGATIVE: // no
                    Collect.getInstance().getActivityLogger().logInstanceAction(this, "createClearDialog", "cancel",
                            qw.getPrompt().getIndex());
                    break;
                }
            }
        };
        alertDialog.setCancelable(false);
        alertDialog.setButton(getString(R.string.discard_answer), quitListener);
        alertDialog.setButton2(getString(R.string.clear_answer_no), quitListener);
        alertDialog.show();
    }

    /**
     * Creates and displays a dialog allowing the user to set the language for
     * the form.
     */
    private void createLanguageDialog() {
        Collect.getInstance().getActivityLogger().logInstanceAction(this, "createLanguageDialog", "show");
        FormController formController = Collect.getInstance().getFormController();
        final String[] languages = formController.getLanguages();
        int selected = -1;
        if (languages != null) {
            String language = formController.getLanguage();
            for (int i = 0; i < languages.length; i++) {
                if (language.equals(languages[i])) {
                    selected = i;
                }
            }
        }
        alertDialog = new AlertDialog.Builder(this)
                .setSingleChoiceItems(languages, selected, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int whichButton) {
                        // Update the language in the content provider
                        // when selecting a new
                        // language
                        ContentValues values = new ContentValues();
                        values.put(FormsColumns.LANGUAGE, languages[whichButton]);
                        String selection = FormsColumns.FORM_FILE_PATH + "=?";
                        String[] selectArgs = { formPath };
                        int updated = formsDao.updateForm(values, selection, selectArgs);
                        Timber.i("Updated language to: %s in %d rows", languages[whichButton], updated);

                        Collect.getInstance().getActivityLogger().logInstanceAction(this, "createLanguageDialog",
                                "changeLanguage." + languages[whichButton]);
                        FormController formController = Collect.getInstance().getFormController();
                        formController.setLanguage(languages[whichButton]);
                        dialog.dismiss();
                        if (formController.currentPromptIsQuestion()) {
                            saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
                        }
                        refreshCurrentView();
                    }
                }).setTitle(getString(R.string.change_language))
                .setNegativeButton(getString(R.string.do_not_change), new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int whichButton) {
                        Collect.getInstance().getActivityLogger().logInstanceAction(this, "createLanguageDialog",
                                "cancel");
                    }
                }).create();
        alertDialog.show();
    }

    /**
     * We use Android's dialog management for loading/saving progress dialogs
     */
    @Override
    protected Dialog onCreateDialog(int id) {
        switch (id) {
        case PROGRESS_DIALOG:
            Timber.i("Creating PROGRESS_DIALOG");
            Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateDialog.PROGRESS_DIALOG",
                    "show");
            progressDialog = new ProgressDialog(this);
            DialogInterface.OnClickListener loadingButtonListener = new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    Collect.getInstance().getActivityLogger().logInstanceAction(this,
                            "onCreateDialog.PROGRESS_DIALOG", "cancel");
                    dialog.dismiss();
                    formLoaderTask.setFormLoaderListener(null);
                    FormLoaderTask t = formLoaderTask;
                    formLoaderTask = null;
                    t.cancel(true);
                    t.destroy();
                    finish();
                }
            };
            progressDialog.setIcon(android.R.drawable.ic_dialog_info);
            progressDialog.setTitle(getString(R.string.loading_form));
            progressDialog.setMessage(getString(R.string.please_wait));
            progressDialog.setIndeterminate(true);
            progressDialog.setCancelable(false);
            progressDialog.setButton(getString(R.string.cancel_loading_form), loadingButtonListener);
            return progressDialog;
        case SAVING_DIALOG:
            Timber.i("Creating SAVING_DIALOG");
            Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateDialog.SAVING_DIALOG",
                    "show");
            progressDialog = new ProgressDialog(this);
            DialogInterface.OnClickListener cancelSavingButtonListener = new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    Collect.getInstance().getActivityLogger().logInstanceAction(this,
                            "onCreateDialog.SAVING_DIALOG", "cancel");
                    dialog.dismiss();
                    cancelSaveToDiskTask();
                }
            };
            progressDialog.setIcon(android.R.drawable.ic_dialog_info);
            progressDialog.setTitle(getString(R.string.saving_form));
            progressDialog.setMessage(getString(R.string.please_wait));
            progressDialog.setIndeterminate(true);
            progressDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
                @Override
                public void onDismiss(DialogInterface dialog) {
                    Collect.getInstance().getActivityLogger().logInstanceAction(this,
                            "onCreateDialog.SAVING_DIALOG", "OnDismissListener");
                    cancelSaveToDiskTask();
                }
            });
            return progressDialog;

        case SAVING_IMAGE_DIALOG:
            progressDialog = new ProgressDialog(this);
            progressDialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE);
            progressDialog.setMessage(getString(R.string.please_wait));
            progressDialog.setCancelable(false);

            return progressDialog;
        }
        return null;
    }

    private void cancelSaveToDiskTask() {
        synchronized (saveDialogLock) {
            if (saveToDiskTask != null) {
                saveToDiskTask.setFormSavedListener(null);
                boolean cancelled = saveToDiskTask.cancel(true);
                Timber.w("Cancelled SaveToDiskTask! (%s)", cancelled);
                saveToDiskTask = null;
            }
        }
    }

    /**
     * Dismiss any showing dialogs that we manually manage.
     */
    private void dismissDialogs() {
        Timber.i("Dismiss dialogs");
        if (alertDialog != null && alertDialog.isShowing()) {
            alertDialog.dismiss();
        }
    }

    @Override
    protected void onPause() {
        FormController formController = Collect.getInstance().getFormController();
        dismissDialogs();
        // make sure we're not already saving to disk. if we are, currentPrompt
        // is getting constantly updated
        if (saveToDiskTask == null || saveToDiskTask.getStatus() == AsyncTask.Status.FINISHED) {
            if (currentView != null && formController != null && formController.currentPromptIsQuestion()) {
                saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
            }
        }
        if (currentView != null && currentView instanceof ODKView) {
            // stop audio if it's playing
            ((ODKView) currentView).stopAudio();
        }

        super.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (errorMessage != null) {
            if (alertDialog != null && !alertDialog.isShowing()) {
                createErrorDialog(errorMessage, EXIT);
            } else {
                return;
            }
        }

        FormController formController = Collect.getInstance().getFormController();
        Collect.getInstance().getActivityLogger().open();

        if (formLoaderTask != null) {
            formLoaderTask.setFormLoaderListener(this);
            if (formController == null && formLoaderTask.getStatus() == AsyncTask.Status.FINISHED) {
                FormController fec = formLoaderTask.getFormController();
                if (fec != null) {
                    loadingComplete(formLoaderTask);
                } else {
                    dismissDialog(PROGRESS_DIALOG);
                    FormLoaderTask t = formLoaderTask;
                    formLoaderTask = null;
                    t.cancel(true);
                    t.destroy();
                    // there is no formController -- fire MainMenu activity?
                    startActivity(new Intent(this, MainMenuActivity.class));
                }
            }
        } else {
            if (formController == null) {
                // there is no formController -- fire MainMenu activity?
                startActivity(new Intent(this, MainMenuActivity.class));
                return;
            } else {
                refreshCurrentView();
            }
        }

        if (saveToDiskTask != null) {
            saveToDiskTask.setFormSavedListener(this);
        }

        // only check the buttons if it's enabled in preferences
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        String navigation = sharedPreferences.getString(PreferenceKeys.KEY_NAVIGATION,
                PreferenceKeys.KEY_NAVIGATION);
        if (navigation.contains(PreferenceKeys.NAVIGATION_BUTTONS)) {
            showNavigationButtons = true;
        }

        if (showNavigationButtons) {
            backButton.setVisibility(View.VISIBLE);
            nextButton.setVisibility(View.VISIBLE);
        } else {
            backButton.setVisibility(View.GONE);
            nextButton.setVisibility(View.GONE);
        }
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
        case KeyEvent.KEYCODE_BACK:
            Collect.getInstance().getActivityLogger().logInstanceAction(this, "onKeyDown.KEYCODE_BACK", "quit");
            createQuitDialog();
            return true;
        case KeyEvent.KEYCODE_DPAD_RIGHT:
            if (event.isAltPressed() && !beenSwiped) {
                beenSwiped = true;
                Collect.getInstance().getActivityLogger().logInstanceAction(this, "onKeyDown.KEYCODE_DPAD_RIGHT",
                        "showNext");
                showNextView();
                return true;
            }
            break;
        case KeyEvent.KEYCODE_DPAD_LEFT:
            if (event.isAltPressed() && !beenSwiped) {
                beenSwiped = true;
                Collect.getInstance().getActivityLogger().logInstanceAction(this, "onKeyDown.KEYCODE_DPAD_LEFT",
                        "showPrevious");
                showPreviousView();
                return true;
            }
            break;
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    protected void onDestroy() {
        if (formLoaderTask != null) {
            formLoaderTask.setFormLoaderListener(null);
            // We have to call cancel to terminate the thread, otherwise it
            // lives on and retains the FEC in memory.
            // but only if it's done, otherwise the thread never returns
            if (formLoaderTask.getStatus() == AsyncTask.Status.FINISHED) {
                FormLoaderTask t = formLoaderTask;
                formLoaderTask = null;
                t.cancel(true);
                t.destroy();
            }
        }
        if (saveToDiskTask != null) {
            saveToDiskTask.setFormSavedListener(null);
            // We have to call cancel to terminate the thread, otherwise it
            // lives on and retains the FEC in memory.
            if (saveToDiskTask.getStatus() == AsyncTask.Status.FINISHED) {
                saveToDiskTask.cancel(true);
                saveToDiskTask = null;
            }
        }

        super.onDestroy();

    }

    private int animationCompletionSet = 0;

    private void afterAllAnimations() {
        Timber.i("afterAllAnimations");
        if (staleView != null) {
            if (staleView instanceof ODKView) {
                // http://code.google.com/p/android/issues/detail?id=8488
                ((ODKView) staleView).recycleDrawables();
            }
            staleView = null;
        }

        if (currentView != null && currentView instanceof ODKView) {
            ((ODKView) currentView).setFocus(this);
        }
        beenSwiped = false;
    }

    @Override
    public void onAnimationEnd(Animation animation) {
        Timber.i("onAnimationEnd %s",
                ((animation == inAnimation) ? "in" : ((animation == outAnimation) ? "out" : "other")));
        if (inAnimation == animation) {
            animationCompletionSet |= 1;
        } else if (outAnimation == animation) {
            animationCompletionSet |= 2;
        } else {
            Timber.e("Unexpected animation");
        }

        if (animationCompletionSet == 3) {
            this.afterAllAnimations();
        }
    }

    @Override
    public void onAnimationRepeat(Animation animation) {
        // Added by AnimationListener interface.
        Timber.i("onAnimationRepeat %s",
                ((animation == inAnimation) ? "in" : ((animation == outAnimation) ? "out" : "other")));
    }

    @Override
    public void onAnimationStart(Animation animation) {
        // Added by AnimationListener interface.
        Timber.i("onAnimationStart %s",
                ((animation == inAnimation) ? "in" : ((animation == outAnimation) ? "out" : "other")));
    }

    /**
     * loadingComplete() is called by FormLoaderTask once it has finished
     * loading a form.
     */
    @Override
    public void loadingComplete(FormLoaderTask task) {
        dismissDialog(PROGRESS_DIALOG);

        final FormController formController = task.getFormController();
        int requestCode = task.getRequestCode(); // these are bogus if
        // pendingActivityResult is
        // false
        int resultCode = task.getResultCode();
        Intent intent = task.getIntent();

        formLoaderTask.setFormLoaderListener(null);
        FormLoaderTask t = formLoaderTask;
        formLoaderTask = null;
        t.cancel(true);
        t.destroy();
        Collect.getInstance().setFormController(formController);
        supportInvalidateOptionsMenu();

        Collect.getInstance().setExternalDataManager(task.getExternalDataManager());

        // Set the language if one has already been set in the past
        String[] languageTest = formController.getLanguages();
        if (languageTest != null) {
            String defaultLanguage = formController.getLanguage();
            String newLanguage = "";
            Cursor c = null;
            try {
                c = formsDao.getFormsCursorForFormFilePath(formPath);
                if (c.getCount() == 1) {
                    c.moveToFirst();
                    newLanguage = c.getString(c.getColumnIndex(FormsColumns.LANGUAGE));
                }
            } finally {
                if (c != null) {
                    c.close();
                }
            }

            // if somehow we end up with a bad language, set it to the default
            try {
                formController.setLanguage(newLanguage);
            } catch (Exception e) {
                Timber.e("Ended up with a bad language. %s", newLanguage);
                formController.setLanguage(defaultLanguage);
            }
        }

        boolean pendingActivityResult = task.hasPendingActivityResult();

        if (pendingActivityResult) {
            // set the current view to whatever group we were at...
            refreshCurrentView();
            // process the pending activity request...
            onActivityResult(requestCode, resultCode, intent);
            return;
        }

        // it can be a normal flow for a pending activity result to restore from
        // a savepoint
        // (the call flow handled by the above if statement). For all other use
        // cases, the
        // user should be notified, as it means they wandered off doing other
        // things then
        // returned to ODK Collect and chose Edit Saved Form, but that the
        // savepoint for that
        // form is newer than the last saved version of their form data.

        boolean hasUsedSavepoint = task.hasUsedSavepoint();

        if (hasUsedSavepoint) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    ToastUtils.showLongToast(R.string.savepoint_used);
                }
            });
        }

        // Set saved answer path
        if (formController.getInstancePath() == null) {

            // Create new answer folder.
            String time = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.ENGLISH)
                    .format(Calendar.getInstance().getTime());
            String file = formPath.substring(formPath.lastIndexOf('/') + 1, formPath.lastIndexOf('.'));
            String path = Collect.INSTANCES_PATH + File.separator + file + "_" + time;
            if (FileUtils.createFolder(path)) {
                formController.setInstancePath(new File(path + File.separator + file + "_" + time + ".xml"));
            }
        } else {
            Intent reqIntent = getIntent();
            boolean showFirst = reqIntent.getBooleanExtra("start", false);

            if (!showFirst) {
                // we've just loaded a saved form, so start in the hierarchy view
                Intent i = new Intent(this, FormHierarchyActivity.class);
                String formMode = reqIntent.getStringExtra(ApplicationConstants.BundleKeys.FORM_MODE);
                if (formMode == null || ApplicationConstants.FormModes.EDIT_SAVED.equalsIgnoreCase(formMode)) {
                    i.putExtra(ApplicationConstants.BundleKeys.FORM_MODE,
                            ApplicationConstants.FormModes.EDIT_SAVED);
                    startActivity(i);
                    return; // so we don't show the intro screen before jumping to the hierarchy
                } else {
                    if (ApplicationConstants.FormModes.VIEW_SENT.equalsIgnoreCase(formMode)) {
                        i.putExtra(ApplicationConstants.BundleKeys.FORM_MODE,
                                ApplicationConstants.FormModes.VIEW_SENT);
                        startActivity(i);
                    }
                    finish();
                }
            }
        }

        refreshCurrentView();
    }

    /**
     * called by the FormLoaderTask if something goes wrong.
     */
    @Override
    public void loadingError(String errorMsg) {
        dismissDialog(PROGRESS_DIALOG);
        if (errorMsg != null) {
            createErrorDialog(errorMsg, EXIT);
        } else {
            createErrorDialog(getString(R.string.parse_error), EXIT);
        }
    }

    /**
     * Called by SavetoDiskTask if everything saves correctly.
     */
    @Override
    public void savingComplete(SaveResult saveResult) {
        dismissDialog(SAVING_DIALOG);

        int saveStatus = saveResult.getSaveResult();
        switch (saveStatus) {
        case SaveToDiskTask.SAVED:
            ToastUtils.showShortToast(R.string.data_saved_ok);
            sendSavedBroadcast();
            break;
        case SaveToDiskTask.SAVED_AND_EXIT:
            ToastUtils.showShortToast(R.string.data_saved_ok);
            sendSavedBroadcast();
            finishReturnInstance();
            break;
        case SaveToDiskTask.SAVE_ERROR:
            String message;
            if (saveResult.getSaveErrorMessage() != null) {
                message = getString(R.string.data_saved_error) + ": " + saveResult.getSaveErrorMessage();
            } else {
                message = getString(R.string.data_saved_error);
            }
            ToastUtils.showLongToast(message);
            break;
        case SaveToDiskTask.ENCRYPTION_ERROR:
            ToastUtils.showLongToast(
                    String.format(getString(R.string.encryption_error_message), saveResult.getSaveErrorMessage()));
            finishReturnInstance();
            break;
        case FormEntryController.ANSWER_CONSTRAINT_VIOLATED:
        case FormEntryController.ANSWER_REQUIRED_BUT_EMPTY:
            refreshCurrentView();

            // get constraint behavior preference value with appropriate default
            String constraintBehavior = PreferenceManager.getDefaultSharedPreferences(this)
                    .getString(PreferenceKeys.KEY_CONSTRAINT_BEHAVIOR, PreferenceKeys.CONSTRAINT_BEHAVIOR_DEFAULT);

            // an answer constraint was violated, so we need to display the proper toast(s)
            // if constraint behavior is on_swipe, this will happen if we do a 'swipe' to the
            // next question
            if (constraintBehavior.equals(PreferenceKeys.CONSTRAINT_BEHAVIOR_ON_SWIPE)) {
                next();
            } else {
                // otherwise, we can get the proper toast(s) by saving with constraint check
                saveAnswersForCurrentScreen(EVALUATE_CONSTRAINTS);
            }

            break;
        }
    }

    @Override
    public void onProgressStep(String stepMessage) {
        this.stepMessage = stepMessage;
        if (progressDialog != null) {
            progressDialog.setMessage(getString(R.string.please_wait) + "\n\n" + stepMessage);
        }
    }

    /**
     * Checks the database to determine if the current instance being edited has
     * already been 'marked completed'. A form can be 'unmarked' complete and
     * then resaved.
     *
     * @return true if form has been marked completed, false otherwise.
     */
    private boolean isInstanceComplete(boolean end) {
        FormController formController = Collect.getInstance().getFormController();
        // default to false if we're mid form
        boolean complete = false;

        // if we're at the end of the form, then check the preferences
        if (end) {
            // First get the value from the preferences
            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
            complete = sharedPreferences.getBoolean(PreferenceKeys.KEY_COMPLETED_DEFAULT, true);
        }

        // Then see if we've already marked this form as complete before
        Cursor c = null;
        try {
            c = new InstancesDao()
                    .getInstancesCursorForFilePath(formController.getInstancePath().getAbsolutePath());
            if (c != null && c.getCount() > 0) {
                c.moveToFirst();
                String status = c.getString(c.getColumnIndex(InstanceColumns.STATUS));
                if (InstanceProviderAPI.STATUS_COMPLETE.compareTo(status) == 0) {
                    complete = true;
                }
            }
        } finally {
            if (c != null) {
                c.close();
            }
        }
        return complete;
    }

    public void next() {
        if (!beenSwiped) {
            beenSwiped = true;
            showNextView();
        }
    }

    /**
     * Returns the instance that was just filled out to the calling activity, if
     * requested.
     */
    private void finishReturnInstance() {
        FormController formController = Collect.getInstance().getFormController();
        String action = getIntent().getAction();
        if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_EDIT.equals(action)) {
            // caller is waiting on a picked form
            Cursor c = null;
            try {
                c = new InstancesDao()
                        .getInstancesCursorForFilePath(formController.getInstancePath().getAbsolutePath());
                if (c.getCount() > 0) {
                    // should only be one...
                    c.moveToFirst();
                    String id = c.getString(c.getColumnIndex(InstanceColumns._ID));
                    Uri instance = Uri.withAppendedPath(InstanceColumns.CONTENT_URI, id);
                    setResult(RESULT_OK, new Intent().setData(instance));
                }
            } finally {
                if (c != null) {
                    c.close();
                }
            }
        }
        finish();
    }

    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        // only check the swipe if it's enabled in preferences
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        String navigation = sharedPreferences.getString(PreferenceKeys.KEY_NAVIGATION,
                PreferenceKeys.NAVIGATION_SWIPE);

        if (navigation.contains(PreferenceKeys.NAVIGATION_SWIPE) && doSwipe) {
            // Looks for user swipes. If the user has swiped, move to the
            // appropriate screen.

            // for all screens a swipe is left/right of at least
            // .25" and up/down of less than .25"
            // OR left/right of > .5"
            DisplayMetrics dm = new DisplayMetrics();
            getWindowManager().getDefaultDisplay().getMetrics(dm);
            int xpixellimit = (int) (dm.xdpi * .25);
            int ypixellimit = (int) (dm.ydpi * .25);

            if (currentView != null && currentView instanceof ODKView) {
                if (((ODKView) currentView).suppressFlingGesture(e1, e2, velocityX, velocityY)) {
                    return false;
                }
            }

            if (beenSwiped) {
                return false;
            }

            if ((Math.abs(e1.getX() - e2.getX()) > xpixellimit && Math.abs(e1.getY() - e2.getY()) < ypixellimit)
                    || Math.abs(e1.getX() - e2.getX()) > xpixellimit * 2) {
                beenSwiped = true;
                if (velocityX > 0) {
                    if (e1.getX() > e2.getX()) {
                        Timber.e("showNextView VelocityX is bogus! %f > %f", e1.getX(), e2.getX());
                        Collect.getInstance().getActivityLogger().logInstanceAction(this, "onFling", "showNext");
                        showNextView();
                    } else {
                        Collect.getInstance().getActivityLogger().logInstanceAction(this, "onFling",
                                "showPrevious");
                        showPreviousView();
                    }
                } else {
                    if (e1.getX() < e2.getX()) {
                        Timber.e("showPreviousView VelocityX is bogus! %f < %f", e1.getX(), e2.getX());
                        Collect.getInstance().getActivityLogger().logInstanceAction(this, "onFling",
                                "showPrevious");
                        showPreviousView();
                    } else {
                        Collect.getInstance().getActivityLogger().logInstanceAction(this, "onFling", "showNext");
                        showNextView();
                    }
                }
                return true;
            }
        }

        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        // The onFling() captures the 'up' event so our view thinks it gets long
        // pressed.
        // We don't wnat that, so cancel it.
        if (currentView != null) {
            currentView.cancelLongPress();
        }
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public void advance() {
        next();
    }

    @Override
    protected void onStart() {
        super.onStart();
        Collect.getInstance().getActivityLogger().logOnStart(this);
    }

    @Override
    protected void onStop() {
        Collect.getInstance().getActivityLogger().logOnStop(this);
        super.onStop();
    }

    private void sendSavedBroadcast() {
        Intent i = new Intent();
        i.setAction("org.odk.collect.android.FormSaved");
        this.sendBroadcast(i);
    }

    @Override
    public void onSavePointError(String errorMessage) {
        if (errorMessage != null && errorMessage.trim().length() > 0) {
            ToastUtils.showLongToast(getString(R.string.save_point_error, errorMessage));
        }
    }

    /**
     * Used whenever we need to show empty view and be able to recognize it from the code
     */
    class EmptyView extends View {

        public EmptyView(Context context) {
            super(context);
        }
    }

    public void allowSwiping(boolean doSwipe) {
        this.doSwipe = doSwipe;
    }
}