Java tutorial
/* * Copyright (C) 2009 University of Washington * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package org.odk.collect.android.activities; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.preference.PreferenceManager; import android.provider.MediaStore.Images; import android.support.v4.view.ViewConfigurationCompat; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.text.InputFilter; import android.text.Spanned; import android.util.DisplayMetrics; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.GestureDetector; import android.view.GestureDetector.OnGestureListener; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewConfiguration; import android.view.ViewGroup.LayoutParams; import android.view.Window; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.AnimationUtils; import android.view.inputmethod.InputMethodManager; import android.widget.CheckBox; import android.widget.EditText; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; import org.javarosa.core.model.FormIndex; import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.form.api.FormEntryCaption; import org.javarosa.form.api.FormEntryController; import org.javarosa.form.api.FormEntryPrompt; import org.odk.collect.android.R; import org.odk.collect.android.application.Collect; import org.odk.collect.android.dao.FormsDao; import org.odk.collect.android.dao.InstancesDao; import org.odk.collect.android.exception.GDriveConnectionException; import org.odk.collect.android.exception.JavaRosaException; import org.odk.collect.android.listeners.AdvanceToNextListener; import org.odk.collect.android.listeners.FormLoaderListener; import org.odk.collect.android.listeners.FormSavedListener; import org.odk.collect.android.listeners.SavePointListener; import org.odk.collect.android.logic.FormController; import org.odk.collect.android.logic.FormController.FailedConstraint; import org.odk.collect.android.preferences.AdminKeys; import org.odk.collect.android.preferences.AdminPreferencesActivity; import org.odk.collect.android.preferences.PreferenceKeys; import org.odk.collect.android.preferences.PreferencesActivity; import org.odk.collect.android.provider.FormsProviderAPI.FormsColumns; import org.odk.collect.android.provider.InstanceProviderAPI; import org.odk.collect.android.provider.InstanceProviderAPI.InstanceColumns; import org.odk.collect.android.tasks.FormLoaderTask; import org.odk.collect.android.tasks.SavePointTask; import org.odk.collect.android.tasks.SaveResult; import org.odk.collect.android.tasks.SaveToDiskTask; import org.odk.collect.android.utilities.ApplicationConstants; import org.odk.collect.android.utilities.FileUtils; import org.odk.collect.android.utilities.MediaUtils; import org.odk.collect.android.utilities.ToastUtils; import org.odk.collect.android.views.ODKView; import org.odk.collect.android.widgets.QuestionWidget; import org.odk.collect.android.widgets.StringWidget; import java.io.File; import java.io.FileFilter; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import timber.log.Timber; /** * FormEntryActivity is responsible for displaying questions, animating * transitions between questions, and allowing the user to enter data. * * @author Carl Hartung (carlhartung@gmail.com) * @author Thomas Smyth, Sassafras Tech Collective (tom@sassafrastech.com; constraint behavior * option) */ public class FormEntryActivity extends AppCompatActivity implements AnimationListener, FormLoaderListener, FormSavedListener, AdvanceToNextListener, OnGestureListener, SavePointListener { // save with every swipe forward or back. Timings indicate this takes .25 // seconds. // if it ever becomes an issue, this value can be changed to save every n'th // screen. private static final int SAVEPOINT_INTERVAL = 1; // Defines for FormEntryActivity private static final boolean EXIT = true; private static final boolean DO_NOT_EXIT = false; private static final boolean EVALUATE_CONSTRAINTS = true; private static final boolean DO_NOT_EVALUATE_CONSTRAINTS = false; // Request codes for returning data from specified intent. public static final int IMAGE_CAPTURE = 1; public static final int BARCODE_CAPTURE = 2; public static final int AUDIO_CAPTURE = 3; public static final int VIDEO_CAPTURE = 4; public static final int LOCATION_CAPTURE = 5; public static final int HIERARCHY_ACTIVITY = 6; public static final int IMAGE_CHOOSER = 7; public static final int AUDIO_CHOOSER = 8; public static final int VIDEO_CHOOSER = 9; public static final int EX_STRING_CAPTURE = 10; public static final int EX_INT_CAPTURE = 11; public static final int EX_DECIMAL_CAPTURE = 12; public static final int DRAW_IMAGE = 13; public static final int SIGNATURE_CAPTURE = 14; public static final int ANNOTATE_IMAGE = 15; public static final int ALIGNED_IMAGE = 16; public static final int BEARING_CAPTURE = 17; public static final int EX_GROUP_CAPTURE = 18; public static final int OSM_CAPTURE = 19; public static final int GEOSHAPE_CAPTURE = 20; public static final int GEOTRACE_CAPTURE = 21; // Extra returned from gp activity public static final String LOCATION_RESULT = "LOCATION_RESULT"; public static final String BEARING_RESULT = "BEARING_RESULT"; public static final String GEOSHAPE_RESULTS = "GEOSHAPE_RESULTS"; public static final String GEOTRACE_RESULTS = "GEOTRACE_RESULTS"; public static final String KEY_INSTANCES = "instances"; public static final String KEY_SUCCESS = "success"; public static final String KEY_ERROR = "error"; // Identifies the gp of the form used to launch form entry public static final String KEY_FORMPATH = "formpath"; // Identifies whether this is a new form, or reloading a form after a screen // rotation (or similar) private static final String NEWFORM = "newform"; // these are only processed if we shut down and are restoring after an // external intent fires public static final String KEY_INSTANCEPATH = "instancepath"; public static final String KEY_XPATH = "xpath"; public static final String KEY_XPATH_WAITING_FOR_DATA = "xpathwaiting"; // Tracks whether we are autosaving public static final String KEY_AUTO_SAVED = "autosaved"; private static final int MENU_LANGUAGES = Menu.FIRST; private static final int MENU_HIERARCHY_VIEW = Menu.FIRST + 1; private static final int MENU_SAVE = Menu.FIRST + 2; private static final int MENU_PREFERENCES = Menu.FIRST + 3; private static final int PROGRESS_DIALOG = 1; private static final int SAVING_DIALOG = 2; private static final int SAVING_IMAGE_DIALOG = 3; private boolean autoSaved; private boolean doSwipe = true; // Random ID private static final int DELETE_REPEAT = 654321; private String formPath; private GestureDetector gestureDetector; private Animation inAnimation; private Animation outAnimation; private View staleView = null; private LinearLayout questionHolder; private View currentView; private AlertDialog alertDialog; private ProgressDialog progressDialog; private String errorMessage; private boolean shownAlertDialogIsGroupRepeat; // used to limit forward/backward swipes to one per question private boolean beenSwiped = false; private final Object saveDialogLock = new Object(); private int viewCount = 0; private FormLoaderTask formLoaderTask; private SaveToDiskTask saveToDiskTask; private ImageButton nextButton; private ImageButton backButton; private String stepMessage = ""; private Toolbar toolbar; private boolean hasHardwareMenu; enum AnimationType { LEFT, RIGHT, FADE } private SharedPreferences adminPreferences; private boolean showNavigationButtons = false; private FormsDao formsDao; /** * Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // must be at the beginning of any activity that can be called from an // external intent try { Collect.createODKDirs(); } catch (RuntimeException e) { createErrorDialog(e.getMessage(), EXIT); return; } setContentView(R.layout.form_entry); formsDao = new FormsDao(); errorMessage = null; beenSwiped = false; alertDialog = null; currentView = null; inAnimation = null; outAnimation = null; gestureDetector = new GestureDetector(this, this); questionHolder = (LinearLayout) findViewById(R.id.questionholder); hasHardwareMenu = ViewConfigurationCompat .hasPermanentMenuKey(ViewConfiguration.get(getApplicationContext())); // get admin preference settings adminPreferences = getSharedPreferences(AdminPreferencesActivity.ADMIN_PREFERENCES, 0); initToolbar(); nextButton = (ImageButton) findViewById(R.id.form_forward_button); nextButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { beenSwiped = true; showNextView(); } }); backButton = (ImageButton) findViewById(R.id.form_back_button); backButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { beenSwiped = true; showPreviousView(); } }); String startingXPath = null; String waitingXPath = null; String instancePath = null; Boolean newForm = true; autoSaved = false; if (savedInstanceState != null) { if (savedInstanceState.containsKey(KEY_FORMPATH)) { formPath = savedInstanceState.getString(KEY_FORMPATH); } if (savedInstanceState.containsKey(KEY_INSTANCEPATH)) { instancePath = savedInstanceState.getString(KEY_INSTANCEPATH); } if (savedInstanceState.containsKey(KEY_XPATH)) { startingXPath = savedInstanceState.getString(KEY_XPATH); Timber.i("startingXPath is: %s", startingXPath); } if (savedInstanceState.containsKey(KEY_XPATH_WAITING_FOR_DATA)) { waitingXPath = savedInstanceState.getString(KEY_XPATH_WAITING_FOR_DATA); Timber.i("waitingXPath is: %s", waitingXPath); } if (savedInstanceState.containsKey(NEWFORM)) { newForm = savedInstanceState.getBoolean(NEWFORM, true); } if (savedInstanceState.containsKey(KEY_ERROR)) { errorMessage = savedInstanceState.getString(KEY_ERROR); } if (savedInstanceState.containsKey(KEY_AUTO_SAVED)) { autoSaved = savedInstanceState.getBoolean(KEY_AUTO_SAVED); } } // If a parse error message is showing then nothing else is loaded // Dialogs mid form just disappear on rotation. if (errorMessage != null) { createErrorDialog(errorMessage, EXIT); return; } // Check to see if this is a screen flip or a new form load. Object data = getLastCustomNonConfigurationInstance(); if (data instanceof FormLoaderTask) { formLoaderTask = (FormLoaderTask) data; } else if (data instanceof SaveToDiskTask) { saveToDiskTask = (SaveToDiskTask) data; } else if (data == null) { if (!newForm) { if (Collect.getInstance().getFormController() != null) { refreshCurrentView(); } else { Timber.w("Reloading form and restoring state."); // we need to launch the form loader to load the form // controller... formLoaderTask = new FormLoaderTask(instancePath, startingXPath, waitingXPath); Collect.getInstance().getActivityLogger().logAction(this, "formReloaded", formPath); // TODO: this doesn' work (dialog does not get removed): // showDialog(PROGRESS_DIALOG); // show dialog before we execute... formLoaderTask.execute(formPath); } return; } // Not a restart from a screen orientation change (or other). Collect.getInstance().setFormController(null); supportInvalidateOptionsMenu(); Intent intent = getIntent(); if (intent != null) { Uri uri = intent.getData(); if (uri != null && getContentResolver().getType(uri).equals(InstanceColumns.CONTENT_ITEM_TYPE)) { // get the formId and version for this instance... String jrFormId = null; String jrVersion = null; { Cursor instanceCursor = null; try { instanceCursor = getContentResolver().query(uri, null, null, null, null); if (instanceCursor.getCount() != 1) { this.createErrorDialog(getString(R.string.bad_uri, uri), EXIT); return; } else { instanceCursor.moveToFirst(); instancePath = instanceCursor.getString( instanceCursor.getColumnIndex(InstanceColumns.INSTANCE_FILE_PATH)); Collect.getInstance().getActivityLogger().logAction(this, "instanceLoaded", instancePath); jrFormId = instanceCursor .getString(instanceCursor.getColumnIndex(InstanceColumns.JR_FORM_ID)); int idxJrVersion = instanceCursor.getColumnIndex(InstanceColumns.JR_VERSION); jrVersion = instanceCursor.isNull(idxJrVersion) ? null : instanceCursor.getString(idxJrVersion); } } finally { if (instanceCursor != null) { instanceCursor.close(); } } } String[] selectionArgs; String selection; if (jrVersion == null) { selectionArgs = new String[] { jrFormId }; selection = FormsColumns.JR_FORM_ID + "=? AND " + FormsColumns.JR_VERSION + " IS NULL"; } else { selectionArgs = new String[] { jrFormId, jrVersion }; selection = FormsColumns.JR_FORM_ID + "=? AND " + FormsColumns.JR_VERSION + "=?"; } { Cursor formCursor = null; try { formCursor = formsDao.getFormsCursor(selection, selectionArgs); if (formCursor.getCount() == 1) { formCursor.moveToFirst(); formPath = formCursor .getString(formCursor.getColumnIndex(FormsColumns.FORM_FILE_PATH)); } else if (formCursor.getCount() < 1) { this.createErrorDialog( getString(R.string.parent_form_not_present, jrFormId) + ((jrVersion == null) ? "" : "\n" + getString(R.string.version) + " " + jrVersion), EXIT); return; } else if (formCursor.getCount() > 1) { // still take the first entry, but warn that // there are multiple rows. // user will need to hand-edit the SQLite // database to fix it. formCursor.moveToFirst(); formPath = formCursor .getString(formCursor.getColumnIndex(FormsColumns.FORM_FILE_PATH)); this.createErrorDialog(getString(R.string.survey_multiple_forms_error), EXIT); return; } } finally { if (formCursor != null) { formCursor.close(); } } } } else if (uri != null && getContentResolver().getType(uri).equals(FormsColumns.CONTENT_ITEM_TYPE)) { Cursor c = null; try { c = getContentResolver().query(uri, null, null, null, null); if (c.getCount() != 1) { this.createErrorDialog(getString(R.string.bad_uri, uri), EXIT); return; } else { c.moveToFirst(); formPath = c.getString(c.getColumnIndex(FormsColumns.FORM_FILE_PATH)); // This is the fill-blank-form code path. // See if there is a savepoint for this form that // has never been // explicitly saved // by the user. If there is, open this savepoint // (resume this filled-in // form). // Savepoints for forms that were explicitly saved // will be recovered // when that // explicitly saved instance is edited via // edit-saved-form. final String filePrefix = formPath.substring(formPath.lastIndexOf('/') + 1, formPath.lastIndexOf('.')) + "_"; final String fileSuffix = ".xml.save"; File cacheDir = new File(Collect.CACHE_PATH); File[] files = cacheDir.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { String name = pathname.getName(); return name.startsWith(filePrefix) && name.endsWith(fileSuffix); } }); // see if any of these savepoints are for a // filled-in form that has never been // explicitly saved by the user... for (int i = 0; i < files.length; ++i) { File candidate = files[i]; String instanceDirName = candidate.getName().substring(0, candidate.getName().length() - fileSuffix.length()); File instanceDir = new File( Collect.INSTANCES_PATH + File.separator + instanceDirName); File instanceFile = new File(instanceDir, instanceDirName + ".xml"); if (instanceDir.exists() && instanceDir.isDirectory() && !instanceFile.exists()) { // yes! -- use this savepoint file instancePath = instanceFile.getAbsolutePath(); break; } } } } finally { if (c != null) { c.close(); } } } else { Timber.e("Unrecognized URI: %s", uri); this.createErrorDialog(getString(R.string.unrecognized_uri, uri), EXIT); return; } formLoaderTask = new FormLoaderTask(instancePath, null, null); Collect.getInstance().getActivityLogger().logAction(this, "formLoaded", formPath); showDialog(PROGRESS_DIALOG); // show dialog before we execute... formLoaderTask.execute(formPath); } } } private void initToolbar() { toolbar = (Toolbar) findViewById(R.id.toolbar); if (hasHardwareMenu) { toolbar.setTitle(getString(R.string.loading_form)); } else { setTitle(getString(R.string.loading_form)); } toolbar.inflateMenu(R.menu.form_menu); toolbar.findViewById(R.id.menu_save).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected", "MENU_SAVE"); // don't exit saveDataToDisk(DO_NOT_EXIT, isInstanceComplete(false), null); } }); toolbar.findViewById(R.id.menu_goto).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { FormController formController = Collect.getInstance().getFormController(); Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected", "MENU_HIERARCHY_VIEW"); if (formController.currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } Intent i = new Intent(FormEntryActivity.this, FormHierarchyActivity.class); i.putExtra(ApplicationConstants.BundleKeys.FORM_MODE, ApplicationConstants.FormModes.EDIT_SAVED); startActivityForResult(i, HIERARCHY_ACTIVITY); } }); if (!hasHardwareMenu) { setSupportActionBar(toolbar); } } /** * Create save-points asynchronously in order to not affect swiping performance * on larger forms. */ private void nonblockingCreateSavePointData() { try { SavePointTask savePointTask = new SavePointTask(this); savePointTask.execute(); } catch (Exception e) { Timber.e("Could not schedule SavePointTask. Perhaps a lot of swiping is taking place?"); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(KEY_FORMPATH, formPath); FormController formController = Collect.getInstance().getFormController(); if (formController != null) { outState.putString(KEY_INSTANCEPATH, formController.getInstancePath().getAbsolutePath()); outState.putString(KEY_XPATH, formController.getXPath(formController.getFormIndex())); FormIndex waiting = formController.getIndexWaitingForData(); if (waiting != null) { outState.putString(KEY_XPATH_WAITING_FOR_DATA, formController.getXPath(waiting)); } // save the instance to a temp path... nonblockingCreateSavePointData(); } outState.putBoolean(NEWFORM, false); outState.putString(KEY_ERROR, errorMessage); outState.putBoolean(KEY_AUTO_SAVED, autoSaved); } @Override protected void onActivityResult(int requestCode, int resultCode, final Intent intent) { super.onActivityResult(requestCode, resultCode, intent); FormController formController = Collect.getInstance().getFormController(); if (formController == null) { // we must be in the midst of a reload of the FormController. // try to save this callback data to the FormLoaderTask if (formLoaderTask != null && formLoaderTask.getStatus() != AsyncTask.Status.FINISHED) { formLoaderTask.setActivityResult(requestCode, resultCode, intent); } else { Timber.e("Got an activityResult without any pending form loader"); } return; } if (resultCode == RESULT_CANCELED) { // request was canceled... if (requestCode != HIERARCHY_ACTIVITY) { ((ODKView) currentView).cancelWaitingForBinaryData(); } return; } // For handling results returned by the Zxing Barcode scanning library IntentResult barcodeScannerResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); if (barcodeScannerResult != null) { if (barcodeScannerResult.getContents() == null) { // request was canceled... Timber.i("QR code scanning cancelled"); } else { String sb = intent.getStringExtra("SCAN_RESULT"); ((ODKView) currentView).setBinaryData(sb); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); refreshCurrentView(); return; } } switch (requestCode) { case OSM_CAPTURE: String osmFileName = intent.getStringExtra("OSM_FILE_NAME"); ((ODKView) currentView).setBinaryData(osmFileName); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case EX_STRING_CAPTURE: case EX_INT_CAPTURE: case EX_DECIMAL_CAPTURE: String key = "value"; boolean exists = intent.getExtras().containsKey(key); if (exists) { Object externalValue = intent.getExtras().get(key); ((ODKView) currentView).setBinaryData(externalValue); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } break; case EX_GROUP_CAPTURE: try { Bundle extras = intent.getExtras(); ((ODKView) currentView).setDataForFields(extras); } catch (JavaRosaException e) { Timber.e(e); createErrorDialog(e.getCause().getMessage(), DO_NOT_EXIT); } break; case DRAW_IMAGE: case ANNOTATE_IMAGE: case SIGNATURE_CAPTURE: case IMAGE_CAPTURE: /* * We saved the image to the tempfile_path, but we really want it to * be in: /sdcard/odk/instances/[current instnace]/something.jpg so * we move it there before inserting it into the content provider. * Once the android image capture bug gets fixed, (read, we move on * from Android 1.6) we want to handle images the audio and video */ // The intent is empty, but we know we saved the image to the temp // file File fi = new File(Collect.TMPFILE_PATH); String instanceFolder = formController.getInstancePath().getParent(); String s = instanceFolder + File.separator + System.currentTimeMillis() + ".jpg"; File nf = new File(s); if (!fi.renameTo(nf)) { Timber.e("Failed to rename %s", fi.getAbsolutePath()); } else { Timber.i("Renamed %s to %s", fi.getAbsolutePath(), nf.getAbsolutePath()); } ((ODKView) currentView).setBinaryData(nf); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case ALIGNED_IMAGE: /* * We saved the image to the tempfile_path; the app returns the full * path to the saved file in the EXTRA_OUTPUT extra. Take that file * and move it into the instance folder. */ String path = intent.getStringExtra(android.provider.MediaStore.EXTRA_OUTPUT); fi = new File(path); instanceFolder = formController.getInstancePath().getParent(); s = instanceFolder + File.separator + System.currentTimeMillis() + ".jpg"; nf = new File(s); if (!fi.renameTo(nf)) { Timber.e("Failed to rename %s", fi.getAbsolutePath()); } else { Timber.i("Renamed %s to %s", fi.getAbsolutePath(), nf.getAbsolutePath()); } ((ODKView) currentView).setBinaryData(nf); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case IMAGE_CHOOSER: /* * We have a saved image somewhere, but we really want it to be in: * /sdcard/odk/instances/[current instnace]/something.jpg so we move * it there before inserting it into the content provider. Once the * android image capture bug gets fixed, (read, we move on from * Android 1.6) we want to handle images the audio and video */ showDialog(SAVING_IMAGE_DIALOG); Runnable runnable = new Runnable() { @Override public void run() { saveChosenImage(intent.getData()); } }; new Thread(runnable).start(); break; case AUDIO_CAPTURE: case VIDEO_CAPTURE: Uri mediaUri = intent.getData(); saveAudioVideoAnswer(mediaUri); String filePath = MediaUtils.getDataColumn(this, mediaUri, null, null); if (filePath != null) { new File(filePath).delete(); } try { getContentResolver().delete(mediaUri, null, null); } catch (Exception e) { Timber.e(e); } break; case AUDIO_CHOOSER: case VIDEO_CHOOSER: saveAudioVideoAnswer(intent.getData()); break; case LOCATION_CAPTURE: String sl = intent.getStringExtra(LOCATION_RESULT); ((ODKView) currentView).setBinaryData(sl); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case GEOSHAPE_CAPTURE: //String ls = intent.getStringExtra(GEOSHAPE_RESULTS); String gshr = intent.getStringExtra(GEOSHAPE_RESULTS); ((ODKView) currentView).setBinaryData(gshr); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case GEOTRACE_CAPTURE: String traceExtra = intent.getStringExtra(GEOTRACE_RESULTS); ((ODKView) currentView).setBinaryData(traceExtra); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case BEARING_CAPTURE: String bearing = intent.getStringExtra(BEARING_RESULT); ((ODKView) currentView).setBinaryData(bearing); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case HIERARCHY_ACTIVITY: // We may have jumped to a new index in hierarchy activity, so // refresh break; } refreshCurrentView(); } private void saveChosenImage(Uri selectedImage) { // Copy file to sdcard String instanceFolder1 = Collect.getInstance().getFormController().getInstancePath().getParent(); String destImagePath = instanceFolder1 + File.separator + System.currentTimeMillis() + ".jpg"; File chosenImage; try { chosenImage = MediaUtils.getFileFromUri(this, selectedImage, Images.Media.DATA); if (chosenImage != null) { final File newImage = new File(destImagePath); FileUtils.copyFile(chosenImage, newImage); runOnUiThread(new Runnable() { @Override public void run() { dismissDialog(SAVING_IMAGE_DIALOG); ((ODKView) currentView).setBinaryData(newImage); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); refreshCurrentView(); } }); } else { runOnUiThread(new Runnable() { @Override public void run() { dismissDialog(SAVING_IMAGE_DIALOG); Timber.e("Could not receive chosen image"); showCustomToast(getString(R.string.error_occured), Toast.LENGTH_SHORT); } }); } } catch (GDriveConnectionException e) { runOnUiThread(new Runnable() { @Override public void run() { dismissDialog(SAVING_IMAGE_DIALOG); Timber.e("Could not receive chosen image due to connection problem"); showCustomToast(getString(R.string.gdrive_connection_exception), Toast.LENGTH_LONG); } }); } } private void saveAudioVideoAnswer(Uri media) { // For audio/video capture/chooser, we get the URI from the content // provider // then the widget copies the file and makes a new entry in the // content provider. ((ODKView) currentView).setBinaryData(media); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } /** * Refreshes the current view. the controller and the displayed view can get * out of sync due to dialogs and restarts caused by screen orientation * changes, so they're resynchronized here. */ public void refreshCurrentView() { FormController formController = Collect.getInstance().getFormController(); int event = formController.getEvent(); // When we refresh, repeat dialog state isn't maintained, so step back // to the previous // question. // Also, if we're within a group labeled 'field list', step back to the // beginning of that // group. // That is, skip backwards over repeat prompts, groups that are not // field-lists, // repeat events, and indexes in field-lists that is not the containing // group. View current = createView(event, false); showView(current, AnimationType.FADE); } @Override public boolean onCreateOptionsMenu(Menu menu) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateOptionsMenu", "show"); super.onCreateOptionsMenu(menu); if (!hasHardwareMenu) { menu.add(0, MENU_SAVE, 0, R.string.save_all_answers).setIcon(android.R.drawable.ic_menu_save) .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); menu.add(0, MENU_HIERARCHY_VIEW, 0, R.string.view_hierarchy).setIcon(R.drawable.ic_menu_goto) .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); } menu.add(0, MENU_LANGUAGES, 0, R.string.change_language).setIcon(R.drawable.ic_menu_start_conversation) .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); menu.add(0, MENU_PREFERENCES, 0, R.string.general_preferences).setIcon(R.drawable.ic_menu_preferences) .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); if (adminPreferences == null) { return false; } boolean useability; if (!hasHardwareMenu) { useability = adminPreferences.getBoolean(AdminKeys.KEY_SAVE_MID, true); menu.findItem(MENU_SAVE).setVisible(useability).setEnabled(useability); useability = adminPreferences.getBoolean(AdminKeys.KEY_JUMP_TO, true); menu.findItem(MENU_HIERARCHY_VIEW).setVisible(useability).setEnabled(useability); } FormController formController = Collect.getInstance().getFormController(); useability = adminPreferences.getBoolean(AdminKeys.KEY_CHANGE_LANGUAGE, true) && (formController != null) && formController.getLanguages() != null && formController.getLanguages().length > 1; menu.findItem(MENU_LANGUAGES).setVisible(useability).setEnabled(useability); useability = adminPreferences.getBoolean(AdminKeys.KEY_ACCESS_SETTINGS, true); menu.findItem(MENU_PREFERENCES).setVisible(useability).setEnabled(useability); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { FormController formController = Collect.getInstance().getFormController(); switch (item.getItemId()) { case MENU_LANGUAGES: Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected", "MENU_LANGUAGES"); createLanguageDialog(); return true; case MENU_SAVE: Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected", "MENU_SAVE"); // don't exit saveDataToDisk(DO_NOT_EXIT, isInstanceComplete(false), null); return true; case MENU_HIERARCHY_VIEW: Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected", "MENU_HIERARCHY_VIEW"); if (formController.currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } Intent i = new Intent(this, FormHierarchyActivity.class); i.putExtra(ApplicationConstants.BundleKeys.FORM_MODE, ApplicationConstants.FormModes.EDIT_SAVED); startActivityForResult(i, HIERARCHY_ACTIVITY); return true; case MENU_PREFERENCES: Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected", "MENU_PREFERENCES"); Intent pref = new Intent(this, PreferencesActivity.class); startActivity(pref); return true; } return super.onOptionsItemSelected(item); } /** * Attempt to save the answer(s) in the current screen to into the data * model. * * @return false if any error occurs while saving (constraint violated, * etc...), true otherwise. */ private boolean saveAnswersForCurrentScreen(boolean evaluateConstraints) { FormController formController = Collect.getInstance().getFormController(); // only try to save if the current event is a question or a field-list group // and current view is an ODKView (occasionally we show blank views that do not have any // controls to save data from) if (formController.currentPromptIsQuestion() && currentView instanceof ODKView) { LinkedHashMap<FormIndex, IAnswerData> answers = ((ODKView) currentView).getAnswers(); try { FailedConstraint constraint = formController.saveAllScreenAnswers(answers, evaluateConstraints); if (constraint != null) { createConstraintToast(constraint.index, constraint.status); return false; } } catch (JavaRosaException e) { Timber.e(e); createErrorDialog(e.getCause().getMessage(), DO_NOT_EXIT); return false; } } return true; } /** * Clears the answer on the screen. */ private void clearAnswer(QuestionWidget qw) { if (qw.getAnswer() != null) { qw.clearAnswer(); } } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateContextMenu", "show"); FormController formController = Collect.getInstance().getFormController(); menu.add(0, v.getId(), 0, getString(R.string.clear_answer)); if (formController.indexContainsRepeatableGroup()) { menu.add(0, DELETE_REPEAT, 0, getString(R.string.delete_repeat)); } menu.setHeaderTitle(getString(R.string.edit_prompt)); } @Override public boolean onContextItemSelected(MenuItem item) { if (item.getItemId() == DELETE_REPEAT) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onContextItemSelected", "createDeleteRepeatConfirmDialog"); createDeleteRepeatConfirmDialog(); } else { /* * We don't have the right view here, so we store the View's ID as the * item ID and loop through the possible views to find the one the user * clicked on. */ boolean shouldClearDialogBeShown; for (QuestionWidget qw : ((ODKView) currentView).getWidgets()) { shouldClearDialogBeShown = false; if (qw instanceof StringWidget) { for (int i = 0; i < qw.getChildCount(); i++) { if (item.getItemId() == qw.getChildAt(i).getId()) { shouldClearDialogBeShown = true; break; } } } else if (item.getItemId() == qw.getId()) { shouldClearDialogBeShown = true; } if (shouldClearDialogBeShown) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onContextItemSelected", "createClearDialog", qw.getPrompt().getIndex()); createClearDialog(qw); break; } } } return super.onContextItemSelected(item); } /** * If we're loading, then we pass the loading thread to our next instance. */ @Override public Object onRetainCustomNonConfigurationInstance() { FormController formController = Collect.getInstance().getFormController(); // if a form is loading, pass the loader task if (formLoaderTask != null && formLoaderTask.getStatus() != AsyncTask.Status.FINISHED) { return formLoaderTask; } // if a form is writing to disk, pass the save to disk task if (saveToDiskTask != null && saveToDiskTask.getStatus() != AsyncTask.Status.FINISHED) { return saveToDiskTask; } // mFormEntryController is static so we don't need to pass it. if (formController != null && formController.currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } return null; } /** * Creates a view given the View type and an event * * @param advancingPage -- true if this results from advancing through the form * @return newly created View */ private View createView(int event, boolean advancingPage) { FormController formController = Collect.getInstance().getFormController(); if (hasHardwareMenu) { toolbar.setTitle(formController.getFormTitle()); } else { setTitle(formController.getFormTitle()); } switch (event) { case FormEntryController.EVENT_BEGINNING_OF_FORM: return createViewForFormBeginning(event, true, formController); case FormEntryController.EVENT_END_OF_FORM: View endView = View.inflate(this, R.layout.form_entry_end, null); ((TextView) endView.findViewById(R.id.description)) .setText(getString(R.string.save_enter_data_description, formController.getFormTitle())); // checkbox for if finished or ready to send final CheckBox instanceComplete = ((CheckBox) endView.findViewById(R.id.mark_finished)); instanceComplete.setChecked(isInstanceComplete(true)); if (!adminPreferences.getBoolean(AdminKeys.KEY_MARK_AS_FINALIZED, true)) { instanceComplete.setVisibility(View.GONE); } // edittext to change the displayed name of the instance final EditText saveAs = (EditText) endView.findViewById(R.id.save_name); // disallow carriage returns in the name InputFilter returnFilter = new InputFilter() { public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { for (int i = start; i < end; i++) { if (Character.getType((source.charAt(i))) == Character.CONTROL) { return ""; } } return null; } }; saveAs.setFilters(new InputFilter[] { returnFilter }); String saveName = formController.getSubmissionMetadata().instanceName; if (saveName == null) { // no meta/instanceName field in the form -- see if we have a // name for this instance from a previous save attempt... if (getContentResolver().getType(getIntent().getData()) == InstanceColumns.CONTENT_ITEM_TYPE) { Uri instanceUri = getIntent().getData(); Cursor instance = null; try { instance = getContentResolver().query(instanceUri, null, null, null, null); if (instance.getCount() == 1) { instance.moveToFirst(); saveName = instance.getString(instance.getColumnIndex(InstanceColumns.DISPLAY_NAME)); } } finally { if (instance != null) { instance.close(); } } } if (saveName == null) { // last resort, default to the form title saveName = formController.getFormTitle(); } // present the prompt to allow user to name the form TextView sa = (TextView) endView.findViewById(R.id.save_form_as); sa.setVisibility(View.VISIBLE); saveAs.setText(saveName); saveAs.setEnabled(true); saveAs.setVisibility(View.VISIBLE); } else { // if instanceName is defined in form, this is the name -- no // revisions // display only the name, not the prompt, and disable edits TextView sa = (TextView) endView.findViewById(R.id.save_form_as); sa.setVisibility(View.GONE); saveAs.setText(saveName); saveAs.setEnabled(false); saveAs.setVisibility(View.VISIBLE); } // override the visibility settings based upon admin preferences if (!adminPreferences.getBoolean(AdminKeys.KEY_SAVE_AS, true)) { saveAs.setVisibility(View.GONE); TextView sa = (TextView) endView.findViewById(R.id.save_form_as); sa.setVisibility(View.GONE); } // Create 'save' button endView.findViewById(R.id.save_exit_button).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createView.saveAndExit", instanceComplete.isChecked() ? "saveAsComplete" : "saveIncomplete"); // Form is marked as 'saved' here. if (saveAs.getText().length() < 1) { ToastUtils.showShortToast(R.string.save_as_error); } else { saveDataToDisk(EXIT, instanceComplete.isChecked(), saveAs.getText().toString()); } } }); if (showNavigationButtons) { backButton.setEnabled(true); nextButton.setEnabled(false); } return endView; case FormEntryController.EVENT_QUESTION: case FormEntryController.EVENT_GROUP: case FormEntryController.EVENT_REPEAT: ODKView odkv = null; // should only be a group here if the event_group is a field-list try { FormEntryPrompt[] prompts = formController.getQuestionPrompts(); FormEntryCaption[] groups = formController.getGroupsForCurrentIndex(); odkv = new ODKView(this, formController.getQuestionPrompts(), groups, advancingPage); Timber.i("Created view for group %s %s", (groups.length > 0 ? groups[groups.length - 1].getLongText() : "[top]"), (prompts.length > 0 ? prompts[0].getQuestionText() : "[no question]")); } catch (RuntimeException e) { Timber.e(e); // this is badness to avoid a crash. try { event = formController.stepToNextScreenEvent(); createErrorDialog(e.getMessage(), DO_NOT_EXIT); } catch (JavaRosaException e1) { Timber.e(e1); createErrorDialog(e.getMessage() + "\n\n" + e1.getCause().getMessage(), DO_NOT_EXIT); } return createView(event, advancingPage); } // Makes a "clear answer" menu pop up on long-click for (QuestionWidget qw : odkv.getWidgets()) { if (!qw.getPrompt().isReadOnly()) { // If it's a StringWidget register all its elements apart from EditText as // we want to enable paste option after long click on the EditText if (qw instanceof StringWidget) { for (int i = 0; i < qw.getChildCount(); i++) { if (!(qw.getChildAt(i) instanceof EditText)) { registerForContextMenu(qw.getChildAt(i)); } } } else { registerForContextMenu(qw); } } } if (showNavigationButtons) { adjustBackNavigationButtonVisibility(); nextButton.setEnabled(true); } return odkv; case FormEntryController.EVENT_PROMPT_NEW_REPEAT: createRepeatDialog(); return new EmptyView(this); default: Timber.e("Attempted to create a view that does not exist."); // this is badness to avoid a crash. try { event = formController.stepToNextScreenEvent(); createErrorDialog(getString(R.string.survey_internal_error), EXIT); } catch (JavaRosaException e) { Timber.e(e); createErrorDialog(e.getCause().getMessage(), EXIT); } return createView(event, advancingPage); } } /** * Disables the back button if it is first question.... */ private void adjustBackNavigationButtonVisibility() { FormController formController = Collect.getInstance().getFormController(); try { boolean firstQuestion = formController .stepToPreviousScreenEvent() == FormEntryController.EVENT_BEGINNING_OF_FORM; backButton.setEnabled(!firstQuestion); formController.stepToNextScreenEvent(); if (formController.getEvent() == FormEntryController.EVENT_PROMPT_NEW_REPEAT) { backButton.setEnabled(true); formController.stepToNextScreenEvent(); } } catch (JavaRosaException e) { backButton.setEnabled(true); Timber.e(e); } } private View createViewForFormBeginning(int event, boolean advancingPage, FormController formController) { try { event = formController.stepToNextScreenEvent(); } catch (JavaRosaException e) { Timber.e(e); createErrorDialog(e.getMessage() + "\n\n" + e.getCause().getMessage(), DO_NOT_EXIT); } return createView(event, advancingPage); } @Override public boolean dispatchTouchEvent(MotionEvent mv) { boolean handled = gestureDetector.onTouchEvent(mv); if (!handled) { return super.dispatchTouchEvent(mv); } return handled; // this is always true } /** * Determines what should be displayed on the screen. Possible options are: * a question, an ask repeat dialog, or the submit screen. Also saves * answers to the data model after checking constraints. */ private void showNextView() { try { FormController formController = Collect.getInstance().getFormController(); // get constraint behavior preference value with appropriate default String constraintBehavior = PreferenceManager.getDefaultSharedPreferences(this) .getString(PreferenceKeys.KEY_CONSTRAINT_BEHAVIOR, PreferenceKeys.CONSTRAINT_BEHAVIOR_DEFAULT); if (formController.currentPromptIsQuestion()) { // if constraint behavior says we should validate on swipe, do so if (constraintBehavior.equals(PreferenceKeys.CONSTRAINT_BEHAVIOR_ON_SWIPE)) { if (!saveAnswersForCurrentScreen(EVALUATE_CONSTRAINTS)) { // A constraint was violated so a dialog should be showing. beenSwiped = false; return; } // otherwise, just save without validating (constraints will be validated on // finalize) } else { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } } View next; int originalEvent = formController.getEvent(); int event = formController.stepToNextScreenEvent(); // Helps prevent transition animation at the end of the form (if user swipes left // she will stay on the same screen) if (originalEvent == event && originalEvent == FormEntryController.EVENT_END_OF_FORM) { beenSwiped = false; return; } switch (event) { case FormEntryController.EVENT_QUESTION: case FormEntryController.EVENT_GROUP: // create a savepoint if ((++viewCount) % SAVEPOINT_INTERVAL == 0) { nonblockingCreateSavePointData(); } next = createView(event, true); showView(next, AnimationType.RIGHT); break; case FormEntryController.EVENT_END_OF_FORM: case FormEntryController.EVENT_REPEAT: case FormEntryController.EVENT_PROMPT_NEW_REPEAT: next = createView(event, true); showView(next, AnimationType.RIGHT); break; case FormEntryController.EVENT_REPEAT_JUNCTURE: Timber.i("Repeat juncture: %s", formController.getFormIndex().getReference()); // skip repeat junctures until we implement them break; default: Timber.w("JavaRosa added a new EVENT type and didn't tell us... shame on them."); break; } } catch (JavaRosaException e) { Timber.e(e); createErrorDialog(e.getCause().getMessage(), DO_NOT_EXIT); } } /** * Determines what should be displayed between a question, or the start * screen and displays the appropriate view. Also saves answers to the data * model without checking constraints. */ private void showPreviousView() { try { FormController formController = Collect.getInstance().getFormController(); // The answer is saved on a back swipe, but question constraints are // ignored. if (formController.currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } if (formController.getEvent() != FormEntryController.EVENT_BEGINNING_OF_FORM) { int event = formController.stepToPreviousScreenEvent(); // If we are the begining of the form, lets revert our actions and ignore // this swipe if (event == FormEntryController.EVENT_BEGINNING_OF_FORM) { event = formController.stepToNextScreenEvent(); beenSwiped = false; if (event != FormEntryController.EVENT_PROMPT_NEW_REPEAT) { // Returning here prevents the same view sliding when user is on the first screen return; } } if (event == FormEntryController.EVENT_GROUP || event == FormEntryController.EVENT_QUESTION) { // create savepoint if ((++viewCount) % SAVEPOINT_INTERVAL == 0) { nonblockingCreateSavePointData(); } } View next = createView(event, false); showView(next, AnimationType.LEFT); } else { beenSwiped = false; } } catch (JavaRosaException e) { Timber.e(e); createErrorDialog(e.getCause().getMessage(), DO_NOT_EXIT); } } /** * Displays the View specified by the parameter 'next', animating both the * current view and next appropriately given the AnimationType. Also updates * the progress bar. */ public void showView(View next, AnimationType from) { // disable notifications... if (inAnimation != null) { inAnimation.setAnimationListener(null); } if (outAnimation != null) { outAnimation.setAnimationListener(null); } // logging of the view being shown is already done, as this was handled // by createView() switch (from) { case RIGHT: inAnimation = AnimationUtils.loadAnimation(this, R.anim.push_left_in); outAnimation = AnimationUtils.loadAnimation(this, R.anim.push_left_out); // if animation is left or right then it was a swipe, and we want to re-save on // entry autoSaved = false; break; case LEFT: inAnimation = AnimationUtils.loadAnimation(this, R.anim.push_right_in); outAnimation = AnimationUtils.loadAnimation(this, R.anim.push_right_out); autoSaved = false; break; case FADE: inAnimation = AnimationUtils.loadAnimation(this, R.anim.fade_in); outAnimation = AnimationUtils.loadAnimation(this, R.anim.fade_out); break; } // complete setup for animations... inAnimation.setAnimationListener(this); outAnimation.setAnimationListener(this); // drop keyboard before transition... if (currentView != null) { InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); inputManager.hideSoftInputFromWindow(currentView.getWindowToken(), 0); } RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); // adjust which view is in the layout container... staleView = currentView; currentView = next; questionHolder.addView(currentView, lp); animationCompletionSet = 0; if (staleView != null) { // start OutAnimation for transition... staleView.startAnimation(outAnimation); // and remove the old view (MUST occur after start of animation!!!) questionHolder.removeView(staleView); } else { animationCompletionSet = 2; } // start InAnimation for transition... currentView.startAnimation(inAnimation); String logString = ""; switch (from) { case RIGHT: logString = "next"; break; case LEFT: logString = "previous"; break; case FADE: logString = "refresh"; break; } Collect.getInstance().getActivityLogger().logInstanceAction(this, "showView", logString); FormController formController = Collect.getInstance().getFormController(); if (formController.getEvent() == FormEntryController.EVENT_QUESTION || formController.getEvent() == FormEntryController.EVENT_GROUP || formController.getEvent() == FormEntryController.EVENT_REPEAT) { FormEntryPrompt[] prompts = Collect.getInstance().getFormController().getQuestionPrompts(); for (FormEntryPrompt p : prompts) { List<TreeElement> attrs = p.getBindAttributes(); for (int i = 0; i < attrs.size(); i++) { if (!autoSaved && "saveIncomplete".equals(attrs.get(i).getName())) { saveDataToDisk(false, false, null, false); autoSaved = true; } } } } } // Hopefully someday we can use managed dialogs when the bugs are fixed /* * Ideally, we'd like to use Android to manage dialogs with onCreateDialog() * and onPrepareDialog(), but dialogs with dynamic content are broken in 1.5 * (cupcake). We do use managed dialogs for our static loading * ProgressDialog. The main issue we noticed and are waiting to see fixed * is: onPrepareDialog() is not called after a screen orientation change. * http://code.google.com/p/android/issues/detail?id=1639 */ // /** * Creates and displays a dialog displaying the violated constraint. */ private void createConstraintToast(FormIndex index, int saveStatus) { FormController formController = Collect.getInstance().getFormController(); String constraintText; switch (saveStatus) { case FormEntryController.ANSWER_CONSTRAINT_VIOLATED: Collect.getInstance().getActivityLogger().logInstanceAction(this, "createConstraintToast.ANSWER_CONSTRAINT_VIOLATED", "show", index); constraintText = formController.getQuestionPromptConstraintText(index); if (constraintText == null) { constraintText = formController.getQuestionPrompt(index) .getSpecialFormQuestionText("constraintMsg"); if (constraintText == null) { constraintText = getString(R.string.invalid_answer_error); } } break; case FormEntryController.ANSWER_REQUIRED_BUT_EMPTY: Collect.getInstance().getActivityLogger().logInstanceAction(this, "createConstraintToast.ANSWER_REQUIRED_BUT_EMPTY", "show", index); constraintText = formController.getQuestionPromptRequiredText(index); if (constraintText == null) { constraintText = formController.getQuestionPrompt(index).getSpecialFormQuestionText("requiredMsg"); if (constraintText == null) { constraintText = getString(R.string.required_answer_error); } } break; default: return; } showCustomToast(constraintText, Toast.LENGTH_SHORT); } /** * Creates a toast with the specified message. */ private void showCustomToast(String message, int duration) { LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.toast_view, null); // set the text in the view TextView tv = (TextView) view.findViewById(R.id.message); tv.setText(message); Toast t = new Toast(this); t.setView(view); t.setDuration(duration); t.setGravity(Gravity.CENTER, 0, 0); t.show(); } /** * Creates and displays a dialog asking the user if they'd like to create a * repeat of the current group. */ private void createRepeatDialog() { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createRepeatDialog", "show"); // In some cases dialog might be present twice because refreshView() is being called // from onResume(). This ensures that we do not preset this modal dialog if it's already // visible. Checking for shownAlertDialogIsGroupRepeat because the same field // alertDialog is being used for all alert dialogs in this activity. if (alertDialog != null && alertDialog.isShowing() && shownAlertDialogIsGroupRepeat) { return; } alertDialog = new AlertDialog.Builder(this).create(); alertDialog.setIcon(android.R.drawable.ic_dialog_info); DialogInterface.OnClickListener repeatListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { shownAlertDialogIsGroupRepeat = false; FormController formController = Collect.getInstance().getFormController(); switch (i) { case DialogInterface.BUTTON_POSITIVE: // yes, repeat Collect.getInstance().getActivityLogger().logInstanceAction(this, "createRepeatDialog", "addRepeat"); try { formController.newRepeat(); } catch (Exception e) { FormEntryActivity.this.createErrorDialog(e.getMessage(), DO_NOT_EXIT); return; } if (!formController.indexIsInFieldList()) { // we are at a REPEAT event that does not have a // field-list appearance // step to the next visible field... // which could be the start of a new repeat group... showNextView(); } else { // we are at a REPEAT event that has a field-list // appearance // just display this REPEAT event's group. refreshCurrentView(); } break; case DialogInterface.BUTTON_NEGATIVE: // no, no repeat Collect.getInstance().getActivityLogger().logInstanceAction(this, "createRepeatDialog", "showNext"); // // Make sure the error dialog will not disappear. // // When showNextView() popups an error dialog (because of a // JavaRosaException) // the issue is that the "add new repeat dialog" is referenced by // alertDialog // like the error dialog. When the "no repeat" is clicked, the error dialog // is shown. Android by default dismisses the dialogs when a button is // clicked, // so instead of closing the first dialog, it closes the second. new Thread() { @Override public void run() { FormEntryActivity.this.runOnUiThread(new Runnable() { @Override public void run() { try { Thread.sleep(500); } catch (InterruptedException e) { //This is rare Timber.e(e); } showNextView(); } }); } }.start(); break; } } }; FormController formController = Collect.getInstance().getFormController(); if (formController.getLastRepeatCount() > 0) { alertDialog.setTitle(getString(R.string.leaving_repeat_ask)); alertDialog.setMessage(getString(R.string.add_another_repeat, formController.getLastGroupText())); alertDialog.setButton(getString(R.string.add_another), repeatListener); alertDialog.setButton2(getString(R.string.leave_repeat_yes), repeatListener); } else { alertDialog.setTitle(getString(R.string.entering_repeat_ask)); alertDialog.setMessage(getString(R.string.add_repeat, formController.getLastGroupText())); alertDialog.setButton(getString(R.string.entering_repeat), repeatListener); alertDialog.setButton2(getString(R.string.add_repeat_no), repeatListener); } alertDialog.setCancelable(false); beenSwiped = false; shownAlertDialogIsGroupRepeat = true; alertDialog.show(); } /** * Creates and displays dialog with the given errorMsg. */ private void createErrorDialog(String errorMsg, final boolean shouldExit) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createErrorDialog", "show." + Boolean.toString(shouldExit)); if (alertDialog != null && alertDialog.isShowing()) { errorMsg = errorMessage + "\n\n" + errorMsg; errorMessage = errorMsg; } else { alertDialog = new AlertDialog.Builder(this).create(); errorMessage = errorMsg; } alertDialog.setIcon(android.R.drawable.ic_dialog_info); alertDialog.setTitle(getString(R.string.error_occured)); alertDialog.setMessage(errorMsg); DialogInterface.OnClickListener errorListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { switch (i) { case DialogInterface.BUTTON_POSITIVE: Collect.getInstance().getActivityLogger().logInstanceAction(this, "createErrorDialog", "OK"); if (shouldExit) { errorMessage = null; finish(); } break; } } }; alertDialog.setCancelable(false); alertDialog.setButton(getString(R.string.ok), errorListener); beenSwiped = false; alertDialog.show(); } /** * Creates a confirm/cancel dialog for deleting repeats. */ private void createDeleteRepeatConfirmDialog() { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createDeleteRepeatConfirmDialog", "show"); FormController formController = Collect.getInstance().getFormController(); alertDialog = new AlertDialog.Builder(this).create(); alertDialog.setIcon(android.R.drawable.ic_dialog_info); String name = formController.getLastRepeatedGroupName(); int repeatcount = formController.getLastRepeatedGroupRepeatCount(); if (repeatcount != -1) { name += " (" + (repeatcount + 1) + ")"; } alertDialog.setTitle(getString(R.string.delete_repeat_ask)); alertDialog.setMessage(getString(R.string.delete_repeat_confirm, name)); DialogInterface.OnClickListener quitListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { FormController formController = Collect.getInstance().getFormController(); switch (i) { case DialogInterface.BUTTON_POSITIVE: // yes Collect.getInstance().getActivityLogger().logInstanceAction(this, "createDeleteRepeatConfirmDialog", "OK"); formController.deleteRepeat(); showNextView(); break; case DialogInterface.BUTTON_NEGATIVE: // no Collect.getInstance().getActivityLogger().logInstanceAction(this, "createDeleteRepeatConfirmDialog", "cancel"); refreshCurrentView(); break; } } }; alertDialog.setCancelable(false); alertDialog.setButton(getString(R.string.discard_group), quitListener); alertDialog.setButton2(getString(R.string.delete_repeat_no), quitListener); alertDialog.show(); } /** * Saves data and writes it to disk. If exit is set, program will exit after * save completes. Complete indicates whether the user has marked the * isntancs as complete. If updatedSaveName is non-null, the instances * content provider is updated with the new name */ // by default, save the current screen private boolean saveDataToDisk(boolean exit, boolean complete, String updatedSaveName) { return saveDataToDisk(exit, complete, updatedSaveName, true); } // but if you want save in the background, can't be current screen private boolean saveDataToDisk(boolean exit, boolean complete, String updatedSaveName, boolean current) { // save current answer if (current) { if (!saveAnswersForCurrentScreen(complete)) { ToastUtils.showShortToast(R.string.data_saved_error); return false; } } synchronized (saveDialogLock) { saveToDiskTask = new SaveToDiskTask(getIntent().getData(), exit, complete, updatedSaveName); saveToDiskTask.setFormSavedListener(this); autoSaved = true; showDialog(SAVING_DIALOG); // show dialog before we execute... saveToDiskTask.execute(); } return true; } /** * Create a dialog with options to save and exit, save, or quit without * saving */ private void createQuitDialog() { String title; { FormController formController = Collect.getInstance().getFormController(); title = (formController == null) ? null : formController.getFormTitle(); if (title == null) { title = getString(R.string.no_form_loaded); } } String[] items; if (adminPreferences.getBoolean(AdminKeys.KEY_SAVE_MID, true)) { items = new String[] { getString(R.string.keep_changes), getString(R.string.do_not_save) }; } else { items = new String[] { getString(R.string.do_not_save) }; } Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog", "show"); alertDialog = new AlertDialog.Builder(this).setIcon(android.R.drawable.ic_dialog_info) .setTitle(getString(R.string.quit_application, title)) .setNeutralButton(getString(R.string.do_not_exit), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog", "cancel"); dialog.cancel(); } }).setItems(items, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { switch (which) { case 0: // save and exit // this is slightly complicated because if the // option is disabled in // the admin menu, then case 0 actually becomes // 'discard and exit' // whereas if it's enabled it's 'save and exit' if (adminPreferences.getBoolean(AdminKeys.KEY_SAVE_MID, true)) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog", "saveAndExit"); saveDataToDisk(EXIT, isInstanceComplete(false), null); } else { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog", "discardAndExit"); removeTempInstance(); finishReturnInstance(); } break; case 1: // discard changes and exit Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog", "discardAndExit"); // close all open databases of external data. Collect.getInstance().getExternalDataManager().close(); removeTempInstance(); finishReturnInstance(); break; case 2:// do nothing Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog", "cancel"); break; } } }).create(); alertDialog.show(); } /** * this method cleans up unneeded files when the user selects 'discard and * exit' */ private void removeTempInstance() { FormController formController = Collect.getInstance().getFormController(); // attempt to remove any scratch file File temp = SaveToDiskTask.savepointFile(formController.getInstancePath()); if (temp.exists()) { temp.delete(); } boolean erase = false; { Cursor c = null; try { c = new InstancesDao() .getInstancesCursorForFilePath(formController.getInstancePath().getAbsolutePath()); erase = (c.getCount() < 1); } finally { if (c != null) { c.close(); } } } // if it's not already saved, erase everything if (erase) { // delete media first String instanceFolder = formController.getInstancePath().getParent(); Timber.i("Attempting to delete: %s", instanceFolder); int images = MediaUtils .deleteImagesInFolderFromMediaProvider(formController.getInstancePath().getParentFile()); int audio = MediaUtils .deleteAudioInFolderFromMediaProvider(formController.getInstancePath().getParentFile()); int video = MediaUtils .deleteVideoInFolderFromMediaProvider(formController.getInstancePath().getParentFile()); Timber.i("Removed from content providers: %d image files, %d audio files and %d audio files.", images, audio, video); File f = new File(instanceFolder); if (f.exists() && f.isDirectory()) { for (File del : f.listFiles()) { Timber.i("Deleting file: %s", del.getAbsolutePath()); del.delete(); } f.delete(); } } } /** * Confirm clear answer dialog */ private void createClearDialog(final QuestionWidget qw) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createClearDialog", "show", qw.getPrompt().getIndex()); alertDialog = new AlertDialog.Builder(this).create(); alertDialog.setIcon(android.R.drawable.ic_dialog_info); alertDialog.setTitle(getString(R.string.clear_answer_ask)); String question = qw.getPrompt().getLongText(); if (question == null) { question = ""; } if (question.length() > 50) { question = question.substring(0, 50) + "..."; } alertDialog.setMessage(getString(R.string.clearanswer_confirm, question)); DialogInterface.OnClickListener quitListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { switch (i) { case DialogInterface.BUTTON_POSITIVE: // yes Collect.getInstance().getActivityLogger().logInstanceAction(this, "createClearDialog", "clearAnswer", qw.getPrompt().getIndex()); clearAnswer(qw); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case DialogInterface.BUTTON_NEGATIVE: // no Collect.getInstance().getActivityLogger().logInstanceAction(this, "createClearDialog", "cancel", qw.getPrompt().getIndex()); break; } } }; alertDialog.setCancelable(false); alertDialog.setButton(getString(R.string.discard_answer), quitListener); alertDialog.setButton2(getString(R.string.clear_answer_no), quitListener); alertDialog.show(); } /** * Creates and displays a dialog allowing the user to set the language for * the form. */ private void createLanguageDialog() { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createLanguageDialog", "show"); FormController formController = Collect.getInstance().getFormController(); final String[] languages = formController.getLanguages(); int selected = -1; if (languages != null) { String language = formController.getLanguage(); for (int i = 0; i < languages.length; i++) { if (language.equals(languages[i])) { selected = i; } } } alertDialog = new AlertDialog.Builder(this) .setSingleChoiceItems(languages, selected, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int whichButton) { // Update the language in the content provider // when selecting a new // language ContentValues values = new ContentValues(); values.put(FormsColumns.LANGUAGE, languages[whichButton]); String selection = FormsColumns.FORM_FILE_PATH + "=?"; String[] selectArgs = { formPath }; int updated = formsDao.updateForm(values, selection, selectArgs); Timber.i("Updated language to: %s in %d rows", languages[whichButton], updated); Collect.getInstance().getActivityLogger().logInstanceAction(this, "createLanguageDialog", "changeLanguage." + languages[whichButton]); FormController formController = Collect.getInstance().getFormController(); formController.setLanguage(languages[whichButton]); dialog.dismiss(); if (formController.currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } refreshCurrentView(); } }).setTitle(getString(R.string.change_language)) .setNegativeButton(getString(R.string.do_not_change), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int whichButton) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createLanguageDialog", "cancel"); } }).create(); alertDialog.show(); } /** * We use Android's dialog management for loading/saving progress dialogs */ @Override protected Dialog onCreateDialog(int id) { switch (id) { case PROGRESS_DIALOG: Timber.i("Creating PROGRESS_DIALOG"); Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateDialog.PROGRESS_DIALOG", "show"); progressDialog = new ProgressDialog(this); DialogInterface.OnClickListener loadingButtonListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateDialog.PROGRESS_DIALOG", "cancel"); dialog.dismiss(); formLoaderTask.setFormLoaderListener(null); FormLoaderTask t = formLoaderTask; formLoaderTask = null; t.cancel(true); t.destroy(); finish(); } }; progressDialog.setIcon(android.R.drawable.ic_dialog_info); progressDialog.setTitle(getString(R.string.loading_form)); progressDialog.setMessage(getString(R.string.please_wait)); progressDialog.setIndeterminate(true); progressDialog.setCancelable(false); progressDialog.setButton(getString(R.string.cancel_loading_form), loadingButtonListener); return progressDialog; case SAVING_DIALOG: Timber.i("Creating SAVING_DIALOG"); Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateDialog.SAVING_DIALOG", "show"); progressDialog = new ProgressDialog(this); DialogInterface.OnClickListener cancelSavingButtonListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateDialog.SAVING_DIALOG", "cancel"); dialog.dismiss(); cancelSaveToDiskTask(); } }; progressDialog.setIcon(android.R.drawable.ic_dialog_info); progressDialog.setTitle(getString(R.string.saving_form)); progressDialog.setMessage(getString(R.string.please_wait)); progressDialog.setIndeterminate(true); progressDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateDialog.SAVING_DIALOG", "OnDismissListener"); cancelSaveToDiskTask(); } }); return progressDialog; case SAVING_IMAGE_DIALOG: progressDialog = new ProgressDialog(this); progressDialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE); progressDialog.setMessage(getString(R.string.please_wait)); progressDialog.setCancelable(false); return progressDialog; } return null; } private void cancelSaveToDiskTask() { synchronized (saveDialogLock) { if (saveToDiskTask != null) { saveToDiskTask.setFormSavedListener(null); boolean cancelled = saveToDiskTask.cancel(true); Timber.w("Cancelled SaveToDiskTask! (%s)", cancelled); saveToDiskTask = null; } } } /** * Dismiss any showing dialogs that we manually manage. */ private void dismissDialogs() { Timber.i("Dismiss dialogs"); if (alertDialog != null && alertDialog.isShowing()) { alertDialog.dismiss(); } } @Override protected void onPause() { FormController formController = Collect.getInstance().getFormController(); dismissDialogs(); // make sure we're not already saving to disk. if we are, currentPrompt // is getting constantly updated if (saveToDiskTask == null || saveToDiskTask.getStatus() == AsyncTask.Status.FINISHED) { if (currentView != null && formController != null && formController.currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } } if (currentView != null && currentView instanceof ODKView) { // stop audio if it's playing ((ODKView) currentView).stopAudio(); } super.onPause(); } @Override protected void onResume() { super.onResume(); if (errorMessage != null) { if (alertDialog != null && !alertDialog.isShowing()) { createErrorDialog(errorMessage, EXIT); } else { return; } } FormController formController = Collect.getInstance().getFormController(); Collect.getInstance().getActivityLogger().open(); if (formLoaderTask != null) { formLoaderTask.setFormLoaderListener(this); if (formController == null && formLoaderTask.getStatus() == AsyncTask.Status.FINISHED) { FormController fec = formLoaderTask.getFormController(); if (fec != null) { loadingComplete(formLoaderTask); } else { dismissDialog(PROGRESS_DIALOG); FormLoaderTask t = formLoaderTask; formLoaderTask = null; t.cancel(true); t.destroy(); // there is no formController -- fire MainMenu activity? startActivity(new Intent(this, MainMenuActivity.class)); } } } else { if (formController == null) { // there is no formController -- fire MainMenu activity? startActivity(new Intent(this, MainMenuActivity.class)); return; } else { refreshCurrentView(); } } if (saveToDiskTask != null) { saveToDiskTask.setFormSavedListener(this); } // only check the buttons if it's enabled in preferences SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); String navigation = sharedPreferences.getString(PreferenceKeys.KEY_NAVIGATION, PreferenceKeys.KEY_NAVIGATION); if (navigation.contains(PreferenceKeys.NAVIGATION_BUTTONS)) { showNavigationButtons = true; } if (showNavigationButtons) { backButton.setVisibility(View.VISIBLE); nextButton.setVisibility(View.VISIBLE); } else { backButton.setVisibility(View.GONE); nextButton.setVisibility(View.GONE); } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_BACK: Collect.getInstance().getActivityLogger().logInstanceAction(this, "onKeyDown.KEYCODE_BACK", "quit"); createQuitDialog(); return true; case KeyEvent.KEYCODE_DPAD_RIGHT: if (event.isAltPressed() && !beenSwiped) { beenSwiped = true; Collect.getInstance().getActivityLogger().logInstanceAction(this, "onKeyDown.KEYCODE_DPAD_RIGHT", "showNext"); showNextView(); return true; } break; case KeyEvent.KEYCODE_DPAD_LEFT: if (event.isAltPressed() && !beenSwiped) { beenSwiped = true; Collect.getInstance().getActivityLogger().logInstanceAction(this, "onKeyDown.KEYCODE_DPAD_LEFT", "showPrevious"); showPreviousView(); return true; } break; } return super.onKeyDown(keyCode, event); } @Override protected void onDestroy() { if (formLoaderTask != null) { formLoaderTask.setFormLoaderListener(null); // We have to call cancel to terminate the thread, otherwise it // lives on and retains the FEC in memory. // but only if it's done, otherwise the thread never returns if (formLoaderTask.getStatus() == AsyncTask.Status.FINISHED) { FormLoaderTask t = formLoaderTask; formLoaderTask = null; t.cancel(true); t.destroy(); } } if (saveToDiskTask != null) { saveToDiskTask.setFormSavedListener(null); // We have to call cancel to terminate the thread, otherwise it // lives on and retains the FEC in memory. if (saveToDiskTask.getStatus() == AsyncTask.Status.FINISHED) { saveToDiskTask.cancel(true); saveToDiskTask = null; } } super.onDestroy(); } private int animationCompletionSet = 0; private void afterAllAnimations() { Timber.i("afterAllAnimations"); if (staleView != null) { if (staleView instanceof ODKView) { // http://code.google.com/p/android/issues/detail?id=8488 ((ODKView) staleView).recycleDrawables(); } staleView = null; } if (currentView != null && currentView instanceof ODKView) { ((ODKView) currentView).setFocus(this); } beenSwiped = false; } @Override public void onAnimationEnd(Animation animation) { Timber.i("onAnimationEnd %s", ((animation == inAnimation) ? "in" : ((animation == outAnimation) ? "out" : "other"))); if (inAnimation == animation) { animationCompletionSet |= 1; } else if (outAnimation == animation) { animationCompletionSet |= 2; } else { Timber.e("Unexpected animation"); } if (animationCompletionSet == 3) { this.afterAllAnimations(); } } @Override public void onAnimationRepeat(Animation animation) { // Added by AnimationListener interface. Timber.i("onAnimationRepeat %s", ((animation == inAnimation) ? "in" : ((animation == outAnimation) ? "out" : "other"))); } @Override public void onAnimationStart(Animation animation) { // Added by AnimationListener interface. Timber.i("onAnimationStart %s", ((animation == inAnimation) ? "in" : ((animation == outAnimation) ? "out" : "other"))); } /** * loadingComplete() is called by FormLoaderTask once it has finished * loading a form. */ @Override public void loadingComplete(FormLoaderTask task) { dismissDialog(PROGRESS_DIALOG); final FormController formController = task.getFormController(); int requestCode = task.getRequestCode(); // these are bogus if // pendingActivityResult is // false int resultCode = task.getResultCode(); Intent intent = task.getIntent(); formLoaderTask.setFormLoaderListener(null); FormLoaderTask t = formLoaderTask; formLoaderTask = null; t.cancel(true); t.destroy(); Collect.getInstance().setFormController(formController); supportInvalidateOptionsMenu(); Collect.getInstance().setExternalDataManager(task.getExternalDataManager()); // Set the language if one has already been set in the past String[] languageTest = formController.getLanguages(); if (languageTest != null) { String defaultLanguage = formController.getLanguage(); String newLanguage = ""; Cursor c = null; try { c = formsDao.getFormsCursorForFormFilePath(formPath); if (c.getCount() == 1) { c.moveToFirst(); newLanguage = c.getString(c.getColumnIndex(FormsColumns.LANGUAGE)); } } finally { if (c != null) { c.close(); } } // if somehow we end up with a bad language, set it to the default try { formController.setLanguage(newLanguage); } catch (Exception e) { Timber.e("Ended up with a bad language. %s", newLanguage); formController.setLanguage(defaultLanguage); } } boolean pendingActivityResult = task.hasPendingActivityResult(); if (pendingActivityResult) { // set the current view to whatever group we were at... refreshCurrentView(); // process the pending activity request... onActivityResult(requestCode, resultCode, intent); return; } // it can be a normal flow for a pending activity result to restore from // a savepoint // (the call flow handled by the above if statement). For all other use // cases, the // user should be notified, as it means they wandered off doing other // things then // returned to ODK Collect and chose Edit Saved Form, but that the // savepoint for that // form is newer than the last saved version of their form data. boolean hasUsedSavepoint = task.hasUsedSavepoint(); if (hasUsedSavepoint) { runOnUiThread(new Runnable() { @Override public void run() { ToastUtils.showLongToast(R.string.savepoint_used); } }); } // Set saved answer path if (formController.getInstancePath() == null) { // Create new answer folder. String time = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.ENGLISH) .format(Calendar.getInstance().getTime()); String file = formPath.substring(formPath.lastIndexOf('/') + 1, formPath.lastIndexOf('.')); String path = Collect.INSTANCES_PATH + File.separator + file + "_" + time; if (FileUtils.createFolder(path)) { formController.setInstancePath(new File(path + File.separator + file + "_" + time + ".xml")); } } else { Intent reqIntent = getIntent(); boolean showFirst = reqIntent.getBooleanExtra("start", false); if (!showFirst) { // we've just loaded a saved form, so start in the hierarchy view Intent i = new Intent(this, FormHierarchyActivity.class); String formMode = reqIntent.getStringExtra(ApplicationConstants.BundleKeys.FORM_MODE); if (formMode == null || ApplicationConstants.FormModes.EDIT_SAVED.equalsIgnoreCase(formMode)) { i.putExtra(ApplicationConstants.BundleKeys.FORM_MODE, ApplicationConstants.FormModes.EDIT_SAVED); startActivity(i); return; // so we don't show the intro screen before jumping to the hierarchy } else { if (ApplicationConstants.FormModes.VIEW_SENT.equalsIgnoreCase(formMode)) { i.putExtra(ApplicationConstants.BundleKeys.FORM_MODE, ApplicationConstants.FormModes.VIEW_SENT); startActivity(i); } finish(); } } } refreshCurrentView(); } /** * called by the FormLoaderTask if something goes wrong. */ @Override public void loadingError(String errorMsg) { dismissDialog(PROGRESS_DIALOG); if (errorMsg != null) { createErrorDialog(errorMsg, EXIT); } else { createErrorDialog(getString(R.string.parse_error), EXIT); } } /** * Called by SavetoDiskTask if everything saves correctly. */ @Override public void savingComplete(SaveResult saveResult) { dismissDialog(SAVING_DIALOG); int saveStatus = saveResult.getSaveResult(); switch (saveStatus) { case SaveToDiskTask.SAVED: ToastUtils.showShortToast(R.string.data_saved_ok); sendSavedBroadcast(); break; case SaveToDiskTask.SAVED_AND_EXIT: ToastUtils.showShortToast(R.string.data_saved_ok); sendSavedBroadcast(); finishReturnInstance(); break; case SaveToDiskTask.SAVE_ERROR: String message; if (saveResult.getSaveErrorMessage() != null) { message = getString(R.string.data_saved_error) + ": " + saveResult.getSaveErrorMessage(); } else { message = getString(R.string.data_saved_error); } ToastUtils.showLongToast(message); break; case SaveToDiskTask.ENCRYPTION_ERROR: ToastUtils.showLongToast( String.format(getString(R.string.encryption_error_message), saveResult.getSaveErrorMessage())); finishReturnInstance(); break; case FormEntryController.ANSWER_CONSTRAINT_VIOLATED: case FormEntryController.ANSWER_REQUIRED_BUT_EMPTY: refreshCurrentView(); // get constraint behavior preference value with appropriate default String constraintBehavior = PreferenceManager.getDefaultSharedPreferences(this) .getString(PreferenceKeys.KEY_CONSTRAINT_BEHAVIOR, PreferenceKeys.CONSTRAINT_BEHAVIOR_DEFAULT); // an answer constraint was violated, so we need to display the proper toast(s) // if constraint behavior is on_swipe, this will happen if we do a 'swipe' to the // next question if (constraintBehavior.equals(PreferenceKeys.CONSTRAINT_BEHAVIOR_ON_SWIPE)) { next(); } else { // otherwise, we can get the proper toast(s) by saving with constraint check saveAnswersForCurrentScreen(EVALUATE_CONSTRAINTS); } break; } } @Override public void onProgressStep(String stepMessage) { this.stepMessage = stepMessage; if (progressDialog != null) { progressDialog.setMessage(getString(R.string.please_wait) + "\n\n" + stepMessage); } } /** * Checks the database to determine if the current instance being edited has * already been 'marked completed'. A form can be 'unmarked' complete and * then resaved. * * @return true if form has been marked completed, false otherwise. */ private boolean isInstanceComplete(boolean end) { FormController formController = Collect.getInstance().getFormController(); // default to false if we're mid form boolean complete = false; // if we're at the end of the form, then check the preferences if (end) { // First get the value from the preferences SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); complete = sharedPreferences.getBoolean(PreferenceKeys.KEY_COMPLETED_DEFAULT, true); } // Then see if we've already marked this form as complete before Cursor c = null; try { c = new InstancesDao() .getInstancesCursorForFilePath(formController.getInstancePath().getAbsolutePath()); if (c != null && c.getCount() > 0) { c.moveToFirst(); String status = c.getString(c.getColumnIndex(InstanceColumns.STATUS)); if (InstanceProviderAPI.STATUS_COMPLETE.compareTo(status) == 0) { complete = true; } } } finally { if (c != null) { c.close(); } } return complete; } public void next() { if (!beenSwiped) { beenSwiped = true; showNextView(); } } /** * Returns the instance that was just filled out to the calling activity, if * requested. */ private void finishReturnInstance() { FormController formController = Collect.getInstance().getFormController(); String action = getIntent().getAction(); if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_EDIT.equals(action)) { // caller is waiting on a picked form Cursor c = null; try { c = new InstancesDao() .getInstancesCursorForFilePath(formController.getInstancePath().getAbsolutePath()); if (c.getCount() > 0) { // should only be one... c.moveToFirst(); String id = c.getString(c.getColumnIndex(InstanceColumns._ID)); Uri instance = Uri.withAppendedPath(InstanceColumns.CONTENT_URI, id); setResult(RESULT_OK, new Intent().setData(instance)); } } finally { if (c != null) { c.close(); } } } finish(); } @Override public boolean onDown(MotionEvent e) { return false; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // only check the swipe if it's enabled in preferences SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); String navigation = sharedPreferences.getString(PreferenceKeys.KEY_NAVIGATION, PreferenceKeys.NAVIGATION_SWIPE); if (navigation.contains(PreferenceKeys.NAVIGATION_SWIPE) && doSwipe) { // Looks for user swipes. If the user has swiped, move to the // appropriate screen. // for all screens a swipe is left/right of at least // .25" and up/down of less than .25" // OR left/right of > .5" DisplayMetrics dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dm); int xpixellimit = (int) (dm.xdpi * .25); int ypixellimit = (int) (dm.ydpi * .25); if (currentView != null && currentView instanceof ODKView) { if (((ODKView) currentView).suppressFlingGesture(e1, e2, velocityX, velocityY)) { return false; } } if (beenSwiped) { return false; } if ((Math.abs(e1.getX() - e2.getX()) > xpixellimit && Math.abs(e1.getY() - e2.getY()) < ypixellimit) || Math.abs(e1.getX() - e2.getX()) > xpixellimit * 2) { beenSwiped = true; if (velocityX > 0) { if (e1.getX() > e2.getX()) { Timber.e("showNextView VelocityX is bogus! %f > %f", e1.getX(), e2.getX()); Collect.getInstance().getActivityLogger().logInstanceAction(this, "onFling", "showNext"); showNextView(); } else { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onFling", "showPrevious"); showPreviousView(); } } else { if (e1.getX() < e2.getX()) { Timber.e("showPreviousView VelocityX is bogus! %f < %f", e1.getX(), e2.getX()); Collect.getInstance().getActivityLogger().logInstanceAction(this, "onFling", "showPrevious"); showPreviousView(); } else { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onFling", "showNext"); showNextView(); } } return true; } } return false; } @Override public void onLongPress(MotionEvent e) { } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // The onFling() captures the 'up' event so our view thinks it gets long // pressed. // We don't wnat that, so cancel it. if (currentView != null) { currentView.cancelLongPress(); } return false; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public void advance() { next(); } @Override protected void onStart() { super.onStart(); Collect.getInstance().getActivityLogger().logOnStart(this); } @Override protected void onStop() { Collect.getInstance().getActivityLogger().logOnStop(this); super.onStop(); } private void sendSavedBroadcast() { Intent i = new Intent(); i.setAction("org.odk.collect.android.FormSaved"); this.sendBroadcast(i); } @Override public void onSavePointError(String errorMessage) { if (errorMessage != null && errorMessage.trim().length() > 0) { ToastUtils.showLongToast(getString(R.string.save_point_error, errorMessage)); } } /** * Used whenever we need to show empty view and be able to recognize it from the code */ class EmptyView extends View { public EmptyView(Context context) { super(context); } } public void allowSwiping(boolean doSwipe) { this.doSwipe = doSwipe; } }