/*
 * 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.Activity;
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.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.MediaStore.Images;
import android.text.InputFilter;
import android.text.Spanned;
import android.util.DisplayMetrics;
import android.util.Log;
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.ViewGroup.LayoutParams;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;

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.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.AdminPreferencesActivity;
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.CompatibilityUtils;
import org.odk.collect.android.utilities.FileUtils;
import org.odk.collect.android.utilities.MediaUtils;
import org.odk.collect.android.views.ODKView;
import org.odk.collect.android.widgets.QuestionWidget;

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

/**
 * 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 Activity implements AnimationListener,
    FormLoaderListener, FormSavedListener, AdvanceToNextListener,
    OnGestureListener, SavePointListener {
  private static final String t = "FormEntryActivity";

  // 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;

  // 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 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 boolean mAutoSaved;

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

  private String mFormPath;
  private GestureDetector mGestureDetector;

  private Animation mInAnimation;
  private Animation mOutAnimation;
  private View mStaleView = null;

  private LinearLayout mQuestionHolder;
  private View mCurrentView;

  private AlertDialog mAlertDialog;
  private ProgressDialog mProgressDialog;
  private String mErrorMessage;

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

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

  private FormLoaderTask mFormLoaderTask;
  private SaveToDiskTask mSaveToDiskTask;

  private ImageButton mNextButton;
  private ImageButton mBackButton;

  private String stepMessage = "";

  enum AnimationType {
    LEFT, RIGHT, FADE
  }

  private SharedPreferences mAdminPreferences;

  /**
   * 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);
    setTitle(getString(R.string.app_name) + " > "
        + getString(R.string.loading_form));

    mErrorMessage = null;

    mBeenSwiped = false;
    mAlertDialog = null;
    mCurrentView = null;
    mInAnimation = null;
    mOutAnimation = null;
    mGestureDetector = new GestureDetector(this, this);
    mQuestionHolder = (LinearLayout) findViewById(R.id.questionholder);

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

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

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

    String startingXPath = null;
    String waitingXPath = null;
    String instancePath = null;
    Boolean newForm = true;
    mAutoSaved = false;
    if (savedInstanceState != null) {
      if (savedInstanceState.containsKey(KEY_FORMPATH)) {
        mFormPath = savedInstanceState.getString(KEY_FORMPATH);
      }
      if (savedInstanceState.containsKey(KEY_INSTANCEPATH)) {
        instancePath = savedInstanceState.getString(KEY_INSTANCEPATH);
      }
      if (savedInstanceState.containsKey(KEY_XPATH)) {
        startingXPath = savedInstanceState.getString(KEY_XPATH);
        Log.i(t, "startingXPath is: " + startingXPath);
      }
      if (savedInstanceState.containsKey(KEY_XPATH_WAITING_FOR_DATA)) {
        waitingXPath = savedInstanceState
            .getString(KEY_XPATH_WAITING_FOR_DATA);
        Log.i(t, "waitingXPath is: " + waitingXPath);
      }
      if (savedInstanceState.containsKey(NEWFORM)) {
        newForm = savedInstanceState.getBoolean(NEWFORM, true);
      }
      if (savedInstanceState.containsKey(KEY_ERROR)) {
        mErrorMessage = savedInstanceState.getString(KEY_ERROR);
      }
      if (savedInstanceState.containsKey(KEY_AUTO_SAVED)) {
        mAutoSaved = 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 (mErrorMessage != null) {
      createErrorDialog(mErrorMessage, EXIT);
      return;
    }

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

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

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

        if (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("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 = getContentResolver().query(
                  FormsColumns.CONTENT_URI, null, selection,
                  selectionArgs, null);
              if (formCursor.getCount() == 1) {
                formCursor.moveToFirst();
                mFormPath = 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();
                mFormPath = 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 (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("Bad URI: " + uri, EXIT);
              return;
            } else {
              c.moveToFirst();
              mFormPath = 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 = mFormPath.substring(
                  mFormPath.lastIndexOf('/') + 1,
                  mFormPath.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 {
          Log.e(t, "unrecognized URI");
          this.createErrorDialog("Unrecognized URI: " + uri, EXIT);
          return;
        }

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

  /**
   * 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) {
      Log.e(t, "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, mFormPath);
    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, mErrorMessage);
    outState.putBoolean(KEY_AUTO_SAVED, mAutoSaved);
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode,
                                  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 (mFormLoaderTask != null
          && mFormLoaderTask.getStatus() != AsyncTask.Status.FINISHED) {
        mFormLoaderTask.setActivityResult(requestCode, resultCode,
            intent);
      } else {
        Log.e(t,
            "Got an activityResult without any pending form loader");
      }
      return;
    }

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

    switch (requestCode) {
      case BARCODE_CAPTURE:
        String sb = intent.getStringExtra("SCAN_RESULT");
        ((ODKView) mCurrentView).setBinaryData(sb);
        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) mCurrentView).setBinaryData(externalValue);
          saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
        }
        break;
      case EX_GROUP_CAPTURE:
        try {
          Bundle extras = intent.getExtras();
          ((ODKView) mCurrentView).setDataForFields(extras);
        } catch (JavaRosaException e) {
          Log.e(t, e.getMessage(), 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 mInstanceFolder = formController.getInstancePath()
            .getParent();
        String s = mInstanceFolder + File.separator
            + System.currentTimeMillis() + ".jpg";

        File nf = new File(s);
        if (!fi.renameTo(nf)) {
          Log.e(t, "Failed to rename " + fi.getAbsolutePath());
        } else {
          Log.i(t,
              "renamed " + fi.getAbsolutePath() + " to "
                  + nf.getAbsolutePath());
        }

        ((ODKView) mCurrentView).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);
        mInstanceFolder = formController.getInstancePath().getParent();
        s = mInstanceFolder + File.separator + System.currentTimeMillis()
            + ".jpg";

        nf = new File(s);
        if (!fi.renameTo(nf)) {
          Log.e(t, "Failed to rename " + fi.getAbsolutePath());
        } else {
          Log.i(t,
              "renamed " + fi.getAbsolutePath() + " to "
                  + nf.getAbsolutePath());
        }

        ((ODKView) mCurrentView).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
			 */

        // get gp of chosen file
        Uri selectedImage = intent.getData();
        String sourceImagePath = MediaUtils.getPathFromUri(this, selectedImage, Images.Media.DATA);

        // Copy file to sdcard
        String mInstanceFolder1 = formController.getInstancePath()
            .getParent();
        String destImagePath = mInstanceFolder1 + File.separator
            + System.currentTimeMillis() + ".jpg";

        File source = new File(sourceImagePath);
        File newImage = new File(destImagePath);
        FileUtils.copyFile(source, newImage);

        ((ODKView) mCurrentView).setBinaryData(newImage);
        saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
        break;
      case AUDIO_CAPTURE:
      case VIDEO_CAPTURE:
      case AUDIO_CHOOSER:
      case VIDEO_CHOOSER:
        // For audio/video capture/chooser, we get the URI from the content
        // provider
        // then the widget copies the file and makes a new entry in the
        // content provider.
        Uri media = intent.getData();
        ((ODKView) mCurrentView).setBinaryData(media);
        saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
        break;
      case LOCATION_CAPTURE:
        String sl = intent.getStringExtra(LOCATION_RESULT);
        ((ODKView) mCurrentView).setBinaryData(sl);
        saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
        break;
      case BEARING_CAPTURE:
        String bearing = intent.getStringExtra(BEARING_RESULT);
        ((ODKView) mCurrentView).setBinaryData(bearing);
        saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
      case HIERARCHY_ACTIVITY:
        // We may have jumped to a new index in hierarchy activity, so
        // refresh
        break;

    }
    refreshCurrentView();
  }

  /**
   * Refreshes the current view. the controller and the displayed view can get out of sync due to dialogs and restarts
   * caused by screen orientation changes, so they're resynchronized here.
   */
  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.
    if (event == FormEntryController.EVENT_PROMPT_NEW_REPEAT) {
      createRepeatDialog();
    } else {
      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);

    CompatibilityUtils.setShowAsAction(
        menu.add(0, MENU_SAVE, 0, R.string.save_all_answers).setIcon(
            android.R.drawable.ic_menu_save),
        MenuItem.SHOW_AS_ACTION_IF_ROOM);

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

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

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

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

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

    boolean useability;
    useability = mAdminPreferences.getBoolean(
        AdminPreferencesActivity.KEY_SAVE_MID, true);

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

    useability = mAdminPreferences.getBoolean(
        AdminPreferencesActivity.KEY_JUMP_TO, true);

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

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

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

    useability = mAdminPreferences.getBoolean(
        AdminPreferencesActivity.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);
        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.
   *
   * @param evaluateConstraints
   *
   * @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
    if (formController.currentPromptIsQuestion()) {
      LinkedHashMap<FormIndex, IAnswerData> answers = ((ODKView) mCurrentView)
          .getAnswers();
      try {
        FailedConstraint constraint = formController.saveAllScreenAnswers(answers, evaluateConstraints);
        if (constraint != null) {
          createConstraintToast(constraint.index, constraint.status);
          return false;
        }
      } catch (JavaRosaException e) {
        Log.e(t, e.getMessage(), 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) {
		/*
		 * We don't have the right view here, so we store the View's ID as the
		 * item ID and loop through the possible views to find the one the user
		 * clicked on.
		 */
    for (QuestionWidget qw : ((ODKView) mCurrentView).getWidgets()) {
      if (item.getItemId() == qw.getId()) {
        Collect.getInstance()
            .getActivityLogger()
            .logInstanceAction(this, "onContextItemSelected",
                "createClearDialog", qw.getPrompt().getIndex());
        createClearDialog(qw);
      }
    }
    if (item.getItemId() == DELETE_REPEAT) {
      Collect.getInstance()
          .getActivityLogger()
          .logInstanceAction(this, "onContextItemSelected",
              "createDeleteRepeatConfirmDialog");
      createDeleteRepeatConfirmDialog();
    }

    return super.onContextItemSelected(item);
  }

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

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

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

  /**
   * Creates a view given the View type and an event
   *
   * @param 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();
    setTitle(getString(R.string.app_name) + " > "
        + formController.getFormTitle());

    switch (event) {
      case FormEntryController.EVENT_BEGINNING_OF_FORM:
        View startView = View
            .inflate(this, R.layout.form_entry_start, null);
        setTitle(getString(R.string.app_name) + " > "
            + formController.getFormTitle());

        Drawable image = null;
        File mediaFolder = formController.getMediaFolder();
        String mediaDir = mediaFolder.getAbsolutePath();
        BitmapDrawable bitImage = null;
        // attempt to load the form-specific logo...
        // this is arbitrarily silly
        bitImage = new BitmapDrawable(getResources(), mediaDir + File.separator
            + "form_logo.png");

        if (bitImage != null && bitImage.getBitmap() != null
            && bitImage.getIntrinsicHeight() > 0
            && bitImage.getIntrinsicWidth() > 0) {
          image = bitImage;
        }

        if (image == null) {
          // show the opendatakit zig...
          // image =
          // getResources().getDrawable(R.drawable.opendatakit_zig);
          ((ImageView) startView.findViewById(R.id.form_start_bling))
              .setVisibility(View.GONE);
        } else {
          ImageView v = ((ImageView) startView
              .findViewById(R.id.form_start_bling));
          v.setImageDrawable(image);
          v.setContentDescription(formController.getFormTitle());
        }

        // change start screen based on navigation prefs
        String navigationChoice = PreferenceManager
            .getDefaultSharedPreferences(this).getString(
                PreferencesActivity.KEY_NAVIGATION,
                PreferencesActivity.KEY_NAVIGATION);
        Boolean useSwipe = false;
        Boolean useButtons = false;
        ImageView ia = ((ImageView) startView
            .findViewById(R.id.image_advance));
        ImageView ib = ((ImageView) startView
            .findViewById(R.id.image_backup));
        TextView ta = ((TextView) startView.findViewById(R.id.text_advance));
        TextView tb = ((TextView) startView.findViewById(R.id.text_backup));
        TextView d = ((TextView) startView.findViewById(R.id.description));

        if (navigationChoice != null) {
          if (navigationChoice
              .contains(PreferencesActivity.NAVIGATION_SWIPE)) {
            useSwipe = true;
          }
          if (navigationChoice
              .contains(PreferencesActivity.NAVIGATION_BUTTONS)) {
            useButtons = true;
          }
        }
        if (useSwipe && !useButtons) {
          d.setText(getString(R.string.swipe_instructions,
              formController.getFormTitle()));
        } else if (useButtons && !useSwipe) {
          ia.setVisibility(View.GONE);
          ib.setVisibility(View.GONE);
          ta.setVisibility(View.GONE);
          tb.setVisibility(View.GONE);
          d.setText(getString(R.string.buttons_instructions,
              formController.getFormTitle()));
        } else {
          d.setText(getString(R.string.swipe_buttons_instructions,
              formController.getFormTitle()));
        }

        if (mBackButton.isShown()) {
          mBackButton.setEnabled(false);
        }
        if (mNextButton.isShown()) {
          mNextButton.setEnabled(true);
        }

        return startView;
      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 (!mAdminPreferences.getBoolean(
            AdminPreferencesActivity.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.setBackgroundColor(Color.WHITE);
          saveAs.setVisibility(View.VISIBLE);
        }

        // override the visibility settings based upon admin preferences
        if (!mAdminPreferences.getBoolean(
            AdminPreferencesActivity.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
        ((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) {
                  Toast.makeText(FormEntryActivity.this,
                      R.string.save_as_error,
                      Toast.LENGTH_SHORT).show();
                } else {
                  saveDataToDisk(EXIT, instanceComplete
                      .isChecked(), saveAs.getText()
                      .toString());
                }
              }
            });

        if (mBackButton.isShown()) {
          mBackButton.setEnabled(true);
        }
        if (mNextButton.isShown()) {
          mNextButton.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);
          Log.i(t,
              "created view for group "
                  + (groups.length > 0 ? groups[groups.length - 1]
                  .getLongText() : "[top]")
                  + " "
                  + (prompts.length > 0 ? prompts[0]
                  .getQuestionText() : "[no question]"));
        } catch (RuntimeException e) {
          Log.e(t, e.getMessage(), e);
          // this is badness to avoid a crash.
          try {
            event = formController.stepToNextScreenEvent();
            createErrorDialog(e.getMessage(), DO_NOT_EXIT);
          } catch (JavaRosaException e1) {
            Log.e(t, e1.getMessage(), 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()) {
            registerForContextMenu(qw);
          }
        }

        if (mBackButton.isShown() && mNextButton.isShown()) {
          mBackButton.setEnabled(true);
          mNextButton.setEnabled(true);
        }
        return odkv;
      default:
        Log.e(t, "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) {
          Log.e(t, e.getMessage(), e);
          createErrorDialog(e.getCause().getMessage(), EXIT);
        }
        return createView(event, advancingPage);
    }
  }

  @Override
  public boolean dispatchTouchEvent(MotionEvent mv) {
    boolean handled = mGestureDetector.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 constraint_behavior = PreferenceManager.getDefaultSharedPreferences(this)
          .getString(PreferencesActivity.KEY_CONSTRAINT_BEHAVIOR,
              PreferencesActivity.CONSTRAINT_BEHAVIOR_DEFAULT);

      if (formController.currentPromptIsQuestion()) {

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

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

      View next;
      int event = formController.stepToNextScreenEvent();


      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:
          next = createView(event, true);
          showView(next, AnimationType.RIGHT);
          break;
        case FormEntryController.EVENT_PROMPT_NEW_REPEAT:
          createRepeatDialog();
          break;
        case FormEntryController.EVENT_REPEAT_JUNCTURE:
          Log.i(t, "repeat juncture: "
              + formController.getFormIndex().getReference());
          // skip repeat junctures until we implement them
          break;
        default:
          Log.w(t,
              "JavaRosa added a new EVENT type and didn't tell us... shame on them.");
          break;
      }
    } catch (JavaRosaException e) {
      Log.e(t, e.getMessage(), 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 (event == FormEntryController.EVENT_BEGINNING_OF_FORM
            || 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 {
        mBeenSwiped = false;
      }
    } catch (JavaRosaException e) {
      Log.e(t, e.getMessage(), 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 (mInAnimation != null) {
      mInAnimation.setAnimationListener(null);
    }
    if (mOutAnimation != null) {
      mOutAnimation.setAnimationListener(null);
    }

    // logging of the view being shown is already done, as this was handled
    // by createView()
    switch (from) {
      case RIGHT:
        mInAnimation = AnimationUtils.loadAnimation(this,
            R.anim.push_left_in);
        mOutAnimation = 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
        mAutoSaved = false;
        break;
      case LEFT:
        mInAnimation = AnimationUtils.loadAnimation(this,
            R.anim.push_right_in);
        mOutAnimation = AnimationUtils.loadAnimation(this,
            R.anim.push_right_out);
        mAutoSaved = false;
        break;
      case FADE:
        mInAnimation = AnimationUtils.loadAnimation(this, R.anim.fade_in);
        mOutAnimation = AnimationUtils.loadAnimation(this, R.anim.fade_out);
        break;
    }

    // complete setup for animations...
    mInAnimation.setAnimationListener(this);
    mOutAnimation.setAnimationListener(this);

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

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

    // adjust which view is in the layout container...
    mStaleView = mCurrentView;
    mCurrentView = next;
    mQuestionHolder.addView(mCurrentView, lp);
    mAnimationCompletionSet = 0;

    if (mStaleView != null) {
      // start OutAnimation for transition...
      mStaleView.startAnimation(mOutAnimation);
      // and remove the old view (MUST occur after start of animation!!!)
      mQuestionHolder.removeView(mStaleView);
    } else {
      mAnimationCompletionSet = 2;
    }
    // start InAnimation for transition...
    mCurrentView.startAnimation(mInAnimation);

    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) {
        Vector<TreeElement> attrs = p.getBindAttributes();
        for (int i = 0; i < attrs.size(); i++) {
          if (!mAutoSaved && "saveIncomplete".equals(attrs.get(i).getName())) {
            saveDataToDisk(false, false, null, false);
            mAutoSaved = 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.
   *
   * @param 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() {
    FormController formController = Collect.getInstance()
        .getFormController();
    Collect.getInstance().getActivityLogger()
        .logInstanceAction(this, "createRepeatDialog", "show");
    mAlertDialog = new AlertDialog.Builder(this).create();
    mAlertDialog.setIcon(android.R.drawable.ic_dialog_info);
    DialogInterface.OnClickListener repeatListener = new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int i) {
        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 mAlertDialog
            // 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) {
                      e.printStackTrace();
                    }
                    showNextView();
                  }
                });
              }
            }.start();

            break;
        }
      }
    };
    if (formController.getLastRepeatCount() > 0) {
      mAlertDialog.setTitle(getString(R.string.leaving_repeat_ask));
      mAlertDialog.setMessage(getString(R.string.add_another_repeat,
          formController.getLastGroupText()));
      mAlertDialog.setButton(getString(R.string.add_another),
          repeatListener);
      mAlertDialog.setButton2(getString(R.string.leave_repeat_yes),
          repeatListener);

    } else {
      mAlertDialog.setTitle(getString(R.string.entering_repeat_ask));
      mAlertDialog.setMessage(getString(R.string.add_repeat,
          formController.getLastGroupText()));
      mAlertDialog.setButton(getString(R.string.entering_repeat),
          repeatListener);
      mAlertDialog.setButton2(getString(R.string.add_repeat_no),
          repeatListener);
    }
    mAlertDialog.setCancelable(false);
    mBeenSwiped = false;
    mAlertDialog.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 (mAlertDialog != null && mAlertDialog.isShowing()) {
      errorMsg = mErrorMessage + "\n\n" + errorMsg;
      mErrorMessage = errorMsg;
    } else {
      mAlertDialog = new AlertDialog.Builder(this).create();
      mErrorMessage = errorMsg;
    }

    mAlertDialog.setIcon(android.R.drawable.ic_dialog_info);
    mAlertDialog.setTitle(getString(R.string.error_occured));
    mAlertDialog.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) {
              mErrorMessage = null;
              finish();
            }
            break;
        }
      }
    };
    mAlertDialog.setCancelable(false);
    mAlertDialog.setButton(getString(R.string.ok), errorListener);
    mBeenSwiped = false;
    mAlertDialog.show();
  }

  /**
   * Creates a confirm/cancel dialog for deleting repeats.
   */
  private void createDeleteRepeatConfirmDialog() {
    Collect.getInstance()
        .getActivityLogger()
        .logInstanceAction(this, "createDeleteRepeatConfirmDialog",
            "show");
    FormController formController = Collect.getInstance()
        .getFormController();
    mAlertDialog = new AlertDialog.Builder(this).create();
    mAlertDialog.setIcon(android.R.drawable.ic_dialog_info);
    String name = formController.getLastRepeatedGroupName();
    int repeatcount = formController.getLastRepeatedGroupRepeatCount();
    if (repeatcount != -1) {
      name += " (" + (repeatcount + 1) + ")";
    }
    mAlertDialog.setTitle(getString(R.string.delete_repeat_ask));
    mAlertDialog
        .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();
            showPreviousView();
            break;
          case DialogInterface.BUTTON_NEGATIVE: // no
            Collect.getInstance()
                .getActivityLogger()
                .logInstanceAction(this,
                    "createDeleteRepeatConfirmDialog", "cancel");
            break;
        }
      }
    };
    mAlertDialog.setCancelable(false);
    mAlertDialog.setButton(getString(R.string.discard_group), quitListener);
    mAlertDialog.setButton2(getString(R.string.delete_repeat_no),
        quitListener);
    mAlertDialog.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)) {
        Toast.makeText(this, getString(R.string.data_saved_error), Toast.LENGTH_SHORT)
            .show();
        return false;
      }
    }

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

    return true;
  }

  /**
   * Create a dialog with options to save and exit, save, or quit without saving
   */
  private void createQuitDialog() {
    FormController formController = Collect.getInstance()
        .getFormController();
    String[] items;
    if (mAdminPreferences.getBoolean(AdminPreferencesActivity.KEY_SAVE_MID,
        true)) {
      String[] two = {getString(R.string.keep_changes),
          getString(R.string.do_not_save)};
      items = two;
    } else {
      String[] one = {getString(R.string.do_not_save)};
      items = one;
    }

    Collect.getInstance().getActivityLogger()
        .logInstanceAction(this, "createQuitDialog", "show");
    mAlertDialog = new AlertDialog.Builder(this)
        .setIcon(android.R.drawable.ic_dialog_info)
        .setTitle(
            getString(R.string.quit_application,
                formController.getFormTitle()))
        .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 (mAdminPreferences
                    .getBoolean(
                        AdminPreferencesActivity.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();
    mAlertDialog.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();
    }

    String selection = InstanceColumns.INSTANCE_FILE_PATH + "=?";
    String[] selectionArgs = {formController.getInstancePath()
        .getAbsolutePath()};

    boolean erase = false;
    {
      Cursor c = null;
      try {
        c = getContentResolver().query(InstanceColumns.CONTENT_URI,
            null, selection, selectionArgs, null);
        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();
      Log.i(t, "attempting to delete: " + instanceFolder);
      int images = MediaUtils
          .deleteImagesInFolderFromMediaProvider(formController
              .getInstancePath().getParentFile());
      int audio = MediaUtils
          .deleteAudioInFolderFromMediaProvider(formController
              .getInstancePath().getParentFile());
      int video = MediaUtils
          .deleteVideoInFolderFromMediaProvider(formController
              .getInstancePath().getParentFile());

      Log.i(t, "removed from content providers: " + images
          + " image files, " + audio + " audio files," + " and "
          + video + " video files.");
      File f = new File(instanceFolder);
      if (f.exists() && f.isDirectory()) {
        for (File del : f.listFiles()) {
          Log.i(t, "deleting file: " + 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());
    mAlertDialog = new AlertDialog.Builder(this).create();
    mAlertDialog.setIcon(android.R.drawable.ic_dialog_info);

    mAlertDialog.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) + "...";
    }

    mAlertDialog.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;
        }
      }
    };
    mAlertDialog.setCancelable(false);
    mAlertDialog
        .setButton(getString(R.string.discard_answer), quitListener);
    mAlertDialog.setButton2(getString(R.string.clear_answer_no),
        quitListener);
    mAlertDialog.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;
        }
      }
    }
    mAlertDialog = new AlertDialog.Builder(this)
        .setSingleChoiceItems(languages, selected,
            new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog,
                                  int whichButton) {
                FormController formController = Collect
                    .getInstance().getFormController();
                // 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[] = {mFormPath};
                int updated = getContentResolver().update(
                    FormsColumns.CONTENT_URI, values,
                    selection, selectArgs);
                Log.i(t, "Updated language to: "
                    + languages[whichButton] + " in "
                    + updated + " rows");

                Collect.getInstance()
                    .getActivityLogger()
                    .logInstanceAction(
                        this,
                        "createLanguageDialog",
                        "changeLanguage."
                            + languages[whichButton]);
                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();
    mAlertDialog.show();
  }

  /**
   * We use Android's dialog management for loading/saving progress dialogs
   */
  @Override
  protected Dialog onCreateDialog(int id) {
    switch (id) {
      case PROGRESS_DIALOG:
        Log.e(t, "Creating PROGRESS_DIALOG");
        Collect.getInstance()
            .getActivityLogger()
            .logInstanceAction(this, "onCreateDialog.PROGRESS_DIALOG",
                "show");
        mProgressDialog = 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();
            mFormLoaderTask.setFormLoaderListener(null);
            FormLoaderTask t = mFormLoaderTask;
            mFormLoaderTask = null;
            t.cancel(true);
            t.destroy();
            finish();
          }
        };
        mProgressDialog.setIcon(android.R.drawable.ic_dialog_info);
        mProgressDialog.setTitle(getString(R.string.loading_form));
        mProgressDialog.setMessage(getString(R.string.please_wait));
        mProgressDialog.setIndeterminate(true);
        mProgressDialog.setCancelable(false);
        mProgressDialog.setButton(getString(R.string.cancel_loading_form),
            loadingButtonListener);
        return mProgressDialog;
      case SAVING_DIALOG:
        Log.e(t, "Creating SAVING_DIALOG");
        Collect.getInstance()
            .getActivityLogger()
            .logInstanceAction(this, "onCreateDialog.SAVING_DIALOG",
                "show");
        mProgressDialog = 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();
          }
        };
        mProgressDialog.setIcon(android.R.drawable.ic_dialog_info);
        mProgressDialog.setTitle(getString(R.string.saving_form));
        mProgressDialog.setMessage(getString(R.string.please_wait));
        mProgressDialog.setIndeterminate(true);
        mProgressDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
          @Override
          public void onDismiss(DialogInterface dialog) {
            Collect.getInstance()
                .getActivityLogger()
                .logInstanceAction(this,
                    "onCreateDialog.SAVING_DIALOG", "OnDismissListener");
            cancelSaveToDiskTask();
          }
        });
        return mProgressDialog;
    }
    return null;
  }

  private void cancelSaveToDiskTask() {
    synchronized (saveDialogLock) {
      mSaveToDiskTask.setFormSavedListener(null);
      boolean cancelled = mSaveToDiskTask.cancel(true);
      Log.w(t, "Cancelled SaveToDiskTask! (" + cancelled + ")");
      mSaveToDiskTask = null;
    }
  }

  /**
   * Dismiss any showing dialogs that we manually manage.
   */
  private void dismissDialogs() {
    Log.e(t, "Dismiss dialogs");
    if (mAlertDialog != null && mAlertDialog.isShowing()) {
      mAlertDialog.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 (mSaveToDiskTask == null
        || mSaveToDiskTask.getStatus() == AsyncTask.Status.FINISHED) {
      if (mCurrentView != null && formController != null
          && formController.currentPromptIsQuestion()) {
        saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS);
      }
    }

    super.onPause();
  }

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

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

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

    if (mFormLoaderTask != null) {
      mFormLoaderTask.setFormLoaderListener(this);
      if (formController == null
          && mFormLoaderTask.getStatus() == AsyncTask.Status.FINISHED) {
        FormController fec = mFormLoaderTask.getFormController();
        if (fec != null) {
          loadingComplete(mFormLoaderTask);
        } else {
          dismissDialog(PROGRESS_DIALOG);
          FormLoaderTask t = mFormLoaderTask;
          mFormLoaderTask = 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 (mSaveToDiskTask != null) {
      mSaveToDiskTask.setFormSavedListener(this);
    }

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

    if (showButtons) {
      mBackButton.setVisibility(View.VISIBLE);
      mNextButton.setVisibility(View.VISIBLE);
    } else {
      mBackButton.setVisibility(View.GONE);
      mNextButton.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() && !mBeenSwiped) {
          mBeenSwiped = true;
          Collect.getInstance()
              .getActivityLogger()
              .logInstanceAction(this,
                  "onKeyDown.KEYCODE_DPAD_RIGHT", "showNext");
          showNextView();
          return true;
        }
        break;
      case KeyEvent.KEYCODE_DPAD_LEFT:
        if (event.isAltPressed() && !mBeenSwiped) {
          mBeenSwiped = 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 (mFormLoaderTask != null) {
      mFormLoaderTask.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 (mFormLoaderTask.getStatus() == AsyncTask.Status.FINISHED) {
        FormLoaderTask t = mFormLoaderTask;
        mFormLoaderTask = null;
        t.cancel(true);
        t.destroy();
      }
    }
    if (mSaveToDiskTask != null) {
      mSaveToDiskTask.setFormSavedListener(null);
      // We have to call cancel to terminate the thread, otherwise it
      // lives on and retains the FEC in memory.
      if (mSaveToDiskTask.getStatus() == AsyncTask.Status.FINISHED) {
        mSaveToDiskTask.cancel(true);
        mSaveToDiskTask = null;
      }
    }

    super.onDestroy();

  }

  private int mAnimationCompletionSet = 0;

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

    if (mCurrentView instanceof ODKView) {
      ((ODKView) mCurrentView).setFocus(this);
    }
    mBeenSwiped = false;
  }

  @Override
  public void onAnimationEnd(Animation animation) {
    Log.i(t, "onAnimationEnd "
        + ((animation == mInAnimation) ? "in"
        : ((animation == mOutAnimation) ? "out" : "other")));
    if (mInAnimation == animation) {
      mAnimationCompletionSet |= 1;
    } else if (mOutAnimation == animation) {
      mAnimationCompletionSet |= 2;
    } else {
      Log.e(t, "Unexpected animation");
    }

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

  @Override
  public void onAnimationRepeat(Animation animation) {
    // Added by AnimationListener interface.
    Log.i(t, "onAnimationRepeat "
        + ((animation == mInAnimation) ? "in"
        : ((animation == mOutAnimation) ? "out" : "other")));
  }

  @Override
  public void onAnimationStart(Animation animation) {
    // Added by AnimationListener interface.
    Log.i(t, "onAnimationStart "
        + ((animation == mInAnimation) ? "in"
        : ((animation == mOutAnimation) ? "out" : "other")));
  }

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

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

    mFormLoaderTask.setFormLoaderListener(null);
    FormLoaderTask t = mFormLoaderTask;
    mFormLoaderTask = null;
    t.cancel(true);
    t.destroy();
    Collect.getInstance().setFormController(formController);
    CompatibilityUtils.invalidateOptionsMenu(this);

    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 = "";
      String selection = FormsColumns.FORM_FILE_PATH + "=?";
      String selectArgs[] = {mFormPath};
      Cursor c = null;
      try {
        c = getContentResolver().query(FormsColumns.CONTENT_URI, null,
            selection, selectArgs, null);
        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) {
        formController.setLanguage(defaultLanguage);
      }
    }

    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.
    if (hasUsedSavepoint) {
      runOnUiThread(new Runnable() {
        @Override
        public void run() {
          Toast.makeText(FormEntryActivity.this,
              getString(R.string.savepoint_used),
              Toast.LENGTH_LONG).show();
        }
      });
    }

    // 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 = mFormPath.substring(mFormPath.lastIndexOf('/') + 1,
          mFormPath.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);
        startActivity(i);
        return; // so we don't show the intro screen before jumping to
        // the hierarchy
      }
    }

    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:
        Toast.makeText(this, getString(R.string.data_saved_ok),
            Toast.LENGTH_SHORT).show();
        sendSavedBroadcast();
        break;
      case SaveToDiskTask.SAVED_AND_EXIT:
        Toast.makeText(this, getString(R.string.data_saved_ok),
            Toast.LENGTH_SHORT).show();
        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);
        }
        Toast.makeText(this, message,
            Toast.LENGTH_LONG).show();
        break;
      case FormEntryController.ANSWER_CONSTRAINT_VIOLATED:
      case FormEntryController.ANSWER_REQUIRED_BUT_EMPTY:
        refreshCurrentView();

        // get constraint behavior preference value with appropriate default
        String constraint_behavior = PreferenceManager.getDefaultSharedPreferences(this)
            .getString(PreferencesActivity.KEY_CONSTRAINT_BEHAVIOR,
                PreferencesActivity.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 (constraint_behavior.equals(PreferencesActivity.CONSTRAINT_BEHAVIOR_ON_SWIPE)) {
          next();
        }
        // otherwise, we can get the proper toast(s) by saving with constraint check
        else {
          saveAnswersForCurrentScreen(EVALUATE_CONSTRAINTS);
        }

        break;
    }
  }

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

  /**
   * Attempts to save an answer to the specified index.
   *
   * @param answer
   * @param index
   * @param evaluateConstraints
   *
   * @return status as determined in FormEntryController
   */
  public int saveAnswer(IAnswerData answer, FormIndex index,
                        boolean evaluateConstraints) throws JavaRosaException {
    FormController formController = Collect.getInstance()
        .getFormController();
    if (evaluateConstraints) {
      return formController.answerQuestion(index, answer);
    } else {
      formController.saveAnswer(index, answer);
      return FormEntryController.ANSWER_OK;
    }
  }

  /**
   * 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(
          PreferencesActivity.KEY_COMPLETED_DEFAULT, true);
    }

    // Then see if we've already marked this form as complete before
    String selection = InstanceColumns.INSTANCE_FILE_PATH + "=?";
    String[] selectionArgs = {formController.getInstancePath()
        .getAbsolutePath()};
    Cursor c = null;
    try {
      c = getContentResolver().query(InstanceColumns.CONTENT_URI, null,
          selection, selectionArgs, null);
      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 (!mBeenSwiped) {
      mBeenSwiped = 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
      String selection = InstanceColumns.INSTANCE_FILE_PATH + "=?";
      String[] selectionArgs = {formController.getInstancePath()
          .getAbsolutePath()};
      Cursor c = null;
      try {
        c = getContentResolver().query(InstanceColumns.CONTENT_URI,
            null, selection, selectionArgs, null);
        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(
        PreferencesActivity.KEY_NAVIGATION,
        PreferencesActivity.NAVIGATION_SWIPE);
    Boolean doSwipe = false;
    if (navigation.contains(PreferencesActivity.NAVIGATION_SWIPE)) {
      doSwipe = true;
    }
    if (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 (mCurrentView instanceof ODKView) {
        if (((ODKView) mCurrentView).suppressFlingGesture(e1, e2,
            velocityX, velocityY)) {
          return false;
        }
      }

      if (mBeenSwiped) {
        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) {
        mBeenSwiped = true;
        if (velocityX > 0) {
          if (e1.getX() > e2.getX()) {
            Log.e(t,
                "showNextView VelocityX is bogus! " + 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()) {
            Log.e(t,
                "showPreviousView VelocityX is bogus! "
                    + 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 (mCurrentView != null) {
      mCurrentView.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) {
      Toast.makeText(this, getString(R.string.save_point_error, errorMessage), Toast.LENGTH_LONG).show();
    }
  }
}
