/* * 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 com.radicaldynamic.groupinform.activities; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.Set; import org.javarosa.core.model.FormIndex; import org.javarosa.core.model.data.IAnswerData; import org.javarosa.form.api.FormEntryController; import org.javarosa.model.xform.XFormsModule; import org.javarosa.xpath.XPathTypeMismatchException; import org.odk.collect.android.listeners.AdvanceToNextListener; import org.odk.collect.android.logic.FormController; import org.odk.collect.android.logic.PropertyManager; import org.odk.collect.android.preferences.PreferencesActivity; import org.odk.collect.android.utilities.FileUtils; import org.odk.collect.android.views.ODKView; import org.odk.collect.android.widgets.QuestionWidget; 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.content.DialogInterface.OnDismissListener; import android.database.Cursor; 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.GestureDetector; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ContextMenu.ContextMenuInfo; import android.view.GestureDetector.OnGestureListener; import android.view.View.OnClickListener; import android.view.ViewGroup.LayoutParams; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.animation.Animation.AnimationListener; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import com.radicaldynamic.gcmobile.android.dialogs.InstanceInfoDialog; import com.radicaldynamic.groupinform.R; import com.radicaldynamic.groupinform.application.Collect; import com.radicaldynamic.groupinform.documents.FormDefinition; import com.radicaldynamic.groupinform.documents.FormInstance; import com.radicaldynamic.groupinform.documents.Generic; import com.radicaldynamic.groupinform.listeners.FormLoaderListener; import com.radicaldynamic.groupinform.listeners.FormSavedListener; import com.radicaldynamic.groupinform.tasks.FormLoaderTask; import com.radicaldynamic.groupinform.tasks.SaveToDiskTask; import com.radicaldynamic.groupinform.utilities.FileUtilsExtended; /** * FormEntryActivity is responsible for displaying questions, animating transitions between * questions, and allowing the user to enter data. * * @author Carl Hartung (carlhartung@gmail.com) */ public class FormEntryActivity extends Activity implements AnimationListener, FormLoaderListener, FormSavedListener, AdvanceToNextListener, OnGestureListener { private static final String t = "FormEntryActivity"; // 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; // Extra returned from gp activity public static final String LOCATION_RESULT = "LOCATION_RESULT"; // Identifies the gp of the form used to launch form entry public static final String KEY_FORMPATH = "formpath"; public static final String KEY_INSTANCEPATH = "instancepath"; public static final String KEY_INSTANCES = "instances"; public static final String KEY_SUCCESS = "success"; public static final String KEY_ERROR = "error"; // Identifies whether this is a new form, or reloading a form after a screen // rotation (or similar) private static final String NEWFORM = "newform"; 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; // Random ID private static final int DELETE_REPEAT = 654321; private String mFormPath; public static String mInstancePath; private GestureDetector mGestureDetector; public static FormController mFormController; private Animation mInAnimation; private Animation mOutAnimation; private RelativeLayout mRelativeLayout; 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; private FormLoaderTask mFormLoaderTask; private SaveToDiskTask mSaveToDiskTask; enum AnimationType { LEFT, RIGHT, FADE } // BEGIN custom private static final int REMOVE_DIALOG = 3; private static final int INFO_DIALOG = 4; private static final int MENU_REMOVE = Menu.FIRST + 4; private static final int MENU_INFO = Menu.FIRST + 5; // See onRetainNonConfigurationInstance() private static final String KEY_FORM_DEFINITION = "formdefinition"; private static final String KEY_FORM_INSTANCE = "forminstance"; public static final String KEY_NEXT_INSTANCE = "skiptonextinstance"; public static final String KEY_PREVIOUS_INSTANCE = "skiptopreviousinstance"; public static final String KEY_FINISH_ACTIVITY = "finishactivity"; private FormDefinition mFormDefinition = null; private FormInstance mFormInstance = null; private ArrayList<String> mInstances = new ArrayList<String>(); // END custom /** Called when the activity is first created. */ @SuppressWarnings("unchecked") @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 // BEGIN custom // try { // Collect.createODKDirs(); // } catch (RuntimeException e) { // createErrorDialog(e.getMessage(), EXIT); // return; // } // END custom setContentView(R.layout.form_entry); setTitle(getString(R.string.app_name) + " > " + getString(R.string.loading_form)); mRelativeLayout = (RelativeLayout) findViewById(R.id.rl); mBeenSwiped = false; mAlertDialog = null; mCurrentView = null; mInAnimation = null; mOutAnimation = null; mGestureDetector = new GestureDetector(this); // Load JavaRosa modules. needed to restore forms. new XFormsModule().registerModule(); // needed to override rms property manager org.javarosa.core.services.PropertyManager.setPropertyManager(new PropertyManager( getApplicationContext())); Boolean newForm = true; if (savedInstanceState != null) { if (savedInstanceState.containsKey(KEY_FORMPATH)) { mFormPath = savedInstanceState.getString(KEY_FORMPATH); } if (savedInstanceState.containsKey(NEWFORM)) { newForm = savedInstanceState.getBoolean(NEWFORM, true); } if (savedInstanceState.containsKey(KEY_ERROR)) { mErrorMessage = savedInstanceState.getString(KEY_ERROR); } // BEGIN custom if (savedInstanceState.containsKey(KEY_INSTANCES)) { mInstances = savedInstanceState.getStringArrayList(KEY_INSTANCES); } // END custom } // 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; // BEGIN custom } else if (data instanceof HashMap<?, ?>) { mFormDefinition = (FormDefinition) ((HashMap<String, Generic>) data).get(KEY_FORM_DEFINITION); mFormInstance = (FormInstance) ((HashMap<String, Generic>) data).get(KEY_FORM_INSTANCE); // END custom } else if (data == null) { if (!newForm) { refreshCurrentView(); return; } // Not a restart from a screen orientation change (or other). mFormController = null; mInstancePath = null; Intent intent = getIntent(); if (intent != null) { // BEGIN custom // Uri uri = intent.getData(); // // if (getContentResolver().getType(uri) == InstanceColumns.CONTENT_ITEM_TYPE) { // Cursor instanceCursor = this.managedQuery(uri, null, null, null, null); // if (instanceCursor.getCount() != 1) { // this.createErrorDialog("Bad URI: " + uri, EXIT); // return; // } else { // instanceCursor.moveToFirst(); // instanceCursor.moveToFirst(); // mInstancePath = // instanceCursor.getString(instanceCursor // .getColumnIndex(InstanceColumns.INSTANCE_FILE_PATH)); // // String jrFormId = // instanceCursor.getString(instanceCursor // .getColumnIndex(InstanceColumns.JR_FORM_ID)); // // String[] selectionArgs = { // jrFormId // }; // String selection = FormsColumns.JR_FORM_ID + " like ?"; // // Cursor formCursor = // managedQuery(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("Parent form does not exist", EXIT); // return; // } else if (formCursor.getCount() > 1) { // this.createErrorDialog("More than one possible parent form", EXIT); // return; // } // // } // // } else if (getContentResolver().getType(uri) == FormsColumns.CONTENT_ITEM_TYPE) { // Cursor c = this.managedQuery(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)); // } // } else { // Log.e(t, "unrecognized URI"); // this.createErrorDialog("unrecognized URI: " + uri, EXIT); // return; // } // Set browse list mInstances = intent.getStringArrayListExtra(KEY_INSTANCES); // Create folders for form storage; set form path String formFolder = FileUtilsExtended.FORMS_PATH + File.separator + intent.getStringExtra(KEY_FORMPATH); mFormPath = formFolder + File.separator + intent.getStringExtra(KEY_FORMPATH) + ".xml"; if (intent.hasExtra(KEY_INSTANCEPATH)) { // Create folders for instance storage; set instance path String instanceFolder = FileUtilsExtended.INSTANCES_PATH + File.separator + intent.getStringExtra(KEY_INSTANCEPATH); mInstancePath = instanceFolder + File.separator + intent.getStringExtra(KEY_INSTANCEPATH) + ".xml"; Log.v(Collect.LOGTAG, t + ": InstancePath is " + mInstancePath); } // END custom mFormLoaderTask = new FormLoaderTask(); mFormLoaderTask.execute(mFormPath); showDialog(PROGRESS_DIALOG); } } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(KEY_FORMPATH, mFormPath); outState.putBoolean(NEWFORM, false); outState.putString(KEY_ERROR, mErrorMessage); // BEGIN custom outState.putStringArrayList(KEY_INSTANCES, mInstances); // END custom } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); if (resultCode == RESULT_CANCELED) { // request was canceled, so do nothing return; } ContentValues values; Uri imageURI; switch (requestCode) { case BARCODE_CAPTURE: String sb = intent.getStringExtra("SCAN_RESULT"); ((ODKView) mCurrentView).setBinaryData(sb); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS, false); break; 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 // BEGIN custom // File fi = new File(Collect.TMPFILE_PATH); File fi = new File(FileUtilsExtended.EXTERNAL_CACHE + File.separator + FileUtilsExtended.CAPTURED_IMAGE_FILE); // END custom String mInstanceFolder = mInstancePath.substring(0, mInstancePath.lastIndexOf("/") + 1); String s = mInstanceFolder + "/" + 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()); } // Add the new image to the Media content provider so that the // viewing is fast in Android 2.0+ values = new ContentValues(6); values.put(Images.Media.TITLE, nf.getName()); values.put(Images.Media.DISPLAY_NAME, nf.getName()); values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis()); values.put(Images.Media.MIME_TYPE, "image/jpeg"); values.put(Images.Media.DATA, nf.getAbsolutePath()); imageURI = getContentResolver().insert(Images.Media.EXTERNAL_CONTENT_URI, values); Log.i(t, "Inserting image returned uri = " + imageURI.toString()); ((ODKView) mCurrentView).setBinaryData(imageURI); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS, false); refreshCurrentView(); 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 String sourceImagePath = null; Uri selectedImage = intent.getData(); if (selectedImage.toString().startsWith("file")) { sourceImagePath = selectedImage.toString().substring(6); } else { String[] projection = { Images.Media.DATA }; Cursor cursor = managedQuery(selectedImage, projection, null, null, null); startManagingCursor(cursor); int column_index = cursor.getColumnIndexOrThrow(Images.Media.DATA); cursor.moveToFirst(); sourceImagePath = cursor.getString(column_index); } // Copy file to sdcard String mInstanceFolder1 = mInstancePath.substring(0, mInstancePath.lastIndexOf("/") + 1); String destImagePath = mInstanceFolder1 + "/" + System.currentTimeMillis() + ".jpg"; File source = new File(sourceImagePath); File newImage = new File(destImagePath); FileUtils.copyFile(source, newImage); if (newImage.exists()) { // Add the new image to the Media content provider so that the // viewing is fast in Android 2.0+ values = new ContentValues(6); values.put(Images.Media.TITLE, newImage.getName()); values.put(Images.Media.DISPLAY_NAME, newImage.getName()); values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis()); values.put(Images.Media.MIME_TYPE, "image/jpeg"); values.put(Images.Media.DATA, newImage.getAbsolutePath()); imageURI = getContentResolver().insert(Images.Media.EXTERNAL_CONTENT_URI, values); Log.i(t, "Inserting image returned uri = " + imageURI.toString()); ((ODKView) mCurrentView).setBinaryData(imageURI); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS, false); } else { Log.e(t, "NO IMAGE EXISTS at: " + source.getAbsolutePath()); } refreshCurrentView(); 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, false); refreshCurrentView(); break; case LOCATION_CAPTURE: String sl = intent.getStringExtra(LOCATION_RESULT); ((ODKView) mCurrentView).setBinaryData(sl); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS, false); break; case HIERARCHY_ACTIVITY: // BEGIN custom // // We may have jumped to a new index in hierarchy activity, so refresh // refreshCurrentView(); if (intent == null) { // We may have jumped to a new index in hierarchy activity, so refresh refreshCurrentView(); } else { String a = intent.getAction(); if (a.equals(KEY_NEXT_INSTANCE)) { browseToNextInstance(false); } else if (a.equals(KEY_PREVIOUS_INSTANCE)) { browseToPreviousInstance(); } else if (a.equals(KEY_FINISH_ACTIVITY)) { tidyBeforeFinish(); finish(); } } // END custom break; } } /** * 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() { int event = mFormController.getEvent(); // When we refresh, repeat dialog state isn't maintained, so step back to the previous // question. // Also, if we're within a group labeled 'field list', step back to the beginning of that // group. // That is, skip backwards over repeat prompts, groups that are not field-lists, // repeat events, and indexes in field-lists that is not the containing group. while (event == FormEntryController.EVENT_PROMPT_NEW_REPEAT || (event == FormEntryController.EVENT_GROUP && !mFormController .indexIsInFieldList()) || event == FormEntryController.EVENT_REPEAT || (mFormController.indexIsInFieldList() && !(event == FormEntryController.EVENT_GROUP))) { event = mFormController.stepToPreviousEvent(); } View current = createView(event); showView(current, AnimationType.FADE); } @Override public boolean onPrepareOptionsMenu(Menu menu) { menu.removeItem(MENU_LANGUAGES); menu.removeItem(MENU_HIERARCHY_VIEW); menu.removeItem(MENU_SAVE); // BEGIN custom // menu.removeItem(MENU_PREFERENCES); menu.removeItem(MENU_REMOVE); menu.removeItem(MENU_INFO); // END custom menu.add(0, MENU_SAVE, 0, R.string.save_all_answers).setIcon( android.R.drawable.ic_menu_save); // BEGIN custom // menu.add(0, MENU_INFO, 0, R.string.tf_form_details).setIcon( // android.R.drawable.ic_menu_info_details); // END custom menu.add(0, MENU_HIERARCHY_VIEW, 0, getString(R.string.view_hierarchy)).setIcon( R.drawable.ic_menu_goto); menu.add(0, MENU_LANGUAGES, 0, getString(R.string.change_language)) .setIcon(R.drawable.ic_menu_start_conversation) .setEnabled( (mFormController.getLanguages() == null || mFormController.getLanguages().length == 1) ? false : true); // BEGIN custom // menu.add(0, MENU_PREFERENCES, 0, getString(R.string.general_preferences)).setIcon( // android.R.drawable.ic_menu_preferences); menu.add(0, MENU_INFO, 0, getString(R.string.tf_about_form)).setIcon(R.drawable.ic_menu_info_details); menu.add(0, MENU_REMOVE, 0, getString(R.string.tf_remove_form)).setIcon(R.drawable.ic_menu_delete); // END custom return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case MENU_LANGUAGES: createLanguageDialog(); return true; case MENU_SAVE: // don't exit saveDataToDisk(DO_NOT_EXIT, isInstanceComplete(false), null); return true; case MENU_HIERARCHY_VIEW: if (currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS, false); } Intent i = new Intent(this, FormHierarchyActivity.class); // BEGIN custom i.putStringArrayListExtra(KEY_INSTANCES, mInstances); // END custom startActivityForResult(i, HIERARCHY_ACTIVITY); return true; case MENU_PREFERENCES: Intent pref = new Intent(this, PreferencesActivity.class); startActivity(pref); return true; // BEGIN custom case MENU_INFO: showDialog(INFO_DIALOG); return true; case MENU_REMOVE: showDialog(REMOVE_DIALOG); return true; // END custom } return super.onOptionsItemSelected(item); } /** * @return true if the current View represents a question in the form */ private boolean currentPromptIsQuestion() { return (mFormController.getEvent() == FormEntryController.EVENT_QUESTION || mFormController .getEvent() == FormEntryController.EVENT_GROUP); } /** * 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, boolean ignoreRequiredConstraint) { // only try to save if the current event is a question or a field-list group if (mFormController.getEvent() == FormEntryController.EVENT_QUESTION || (mFormController.getEvent() == FormEntryController.EVENT_GROUP && mFormController .indexIsInFieldList())) { HashMap<FormIndex, IAnswerData> answers = ((ODKView) mCurrentView).getAnswers(); Set<FormIndex> indexKeys = answers.keySet(); for (FormIndex index : indexKeys) { // Within a group, you can only save for question events if (mFormController.getEvent(index) == FormEntryController.EVENT_QUESTION) { int saveStatus = saveAnswer(answers.get(index), index, evaluateConstraints, ignoreRequiredConstraint); if (evaluateConstraints && saveStatus != FormEntryController.ANSWER_OK) { createConstraintToast(mFormController.getQuestionPrompt(index) .getConstraintText(), saveStatus); return false; } } else { Log.w(t, "Attempted to save an index referencing something other than a question: " + index.getReference()); } } } return true; } /** * Clears the answer on the screen. */ private void clearAnswer(QuestionWidget qw) { qw.clearAnswer(); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); menu.add(0, v.getId(), 0, getString(R.string.clear_answer)); if (mFormController.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()) { createClearDialog(qw); } } if (item.getItemId() == DELETE_REPEAT) { createDeleteRepeatConfirmDialog(); } return super.onContextItemSelected(item); } /** * If we're loading, then we pass the loading thread to our next instance. */ @Override public Object onRetainNonConfigurationInstance() { // if a form is loading, pass the loader task if (mFormLoaderTask != null && mFormLoaderTask.getStatus() != AsyncTask.Status.FINISHED) return mFormLoaderTask; // if a form is writing to disk, pass the save to disk task if (mSaveToDiskTask != null && mSaveToDiskTask.getStatus() != AsyncTask.Status.FINISHED) return mSaveToDiskTask; // mFormEntryController is static so we don't need to pass it. if (mFormController != null && currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS, false); } // BEGIN custom // return null; // Avoid refetching documents from database by preserving them HashMap<String, Generic> persistentData = new HashMap<String, Generic>(); persistentData.put(KEY_FORM_DEFINITION, mFormDefinition); persistentData.put(KEY_FORM_INSTANCE, mFormInstance); return persistentData; // END custom } /** * Creates a view given the View type and an event * * @param event * @return newly created View */ private View createView(int event) { setTitle(getString(R.string.app_name) + " > " + mFormController.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) + " > " + mFormController.getFormTitle()); ((TextView) startView.findViewById(R.id.description)).setText(getString( R.string.enter_data_description, mFormController.getFormTitle())); Drawable image = null; // BEGIN custom // String[] projection = { // FormsColumns.FORM_MEDIA_PATH // }; // String selection = FormsColumns.FORM_FILE_PATH + "=?"; // String[] selectionArgs = { // mFormPath // }; // Cursor c = // managedQuery(FormsColumns.CONTENT_URI, projection, selection, selectionArgs, // null); // String mediaDir = null; // if (c.getCount() < 1) { // createErrorDialog("form Doesn't exist", true); // return new View(this); // } else { // c.moveToFirst(); // mediaDir = c.getString(c.getColumnIndex(FormsColumns.FORM_MEDIA_PATH)); // } String mediaDir = FileUtilsExtended.FORMS_PATH + File.separator + mFormDefinition.getId() + File.separator + FileUtilsExtended.MEDIA_DIR + File.separator; // END custom BitmapDrawable bitImage = null; // attempt to load the form-specific logo... // this is arbitrarily silly bitImage = new BitmapDrawable(mediaDir + "/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) startView.findViewById(R.id.form_start_bling)) .setImageDrawable(image); } 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, mFormController.getFormTitle())); // checkbox for if finished or ready to send final CheckBox instanceComplete = ((CheckBox) endView.findViewById(R.id.mark_finished)); instanceComplete.setChecked(isInstanceComplete(true)); // 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 = mFormController.getFormTitle(); // BEGIN custom // if (getContentResolver().getType(getIntent().getData()) == InstanceColumns.CONTENT_ITEM_TYPE) { // Uri instanceUri = getIntent().getData(); // Cursor instance = managedQuery(instanceUri, null, null, null, null); // if (instance.getCount() == 1) { // instance.moveToFirst(); // saveName = // instance.getString(instance // .getColumnIndex(InstanceColumns.DISPLAY_NAME)); // } // } // Retrieve instance name if it has been set if (mFormInstance.getName() != null) { saveName = mFormInstance.getName(); } // END custom saveAs.setText(saveName); // Create 'save' button ((Button) endView.findViewById(R.id.save_exit_button)) .setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // 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 { // BEGIN custom // saveDataToDisk(EXIT, instanceComplete.isChecked(), saveAs // .getText().toString()); /* * Don't ask the SaveToDiskTask to set an instance name if it is * equal to the current form definition name. * * Rationale: if the form definition name changes in the future * we don't want to call instances by the old form name or * display the old form name. This won't make a lot of sense to * the user. * * The current idea is that we might display form instances in the * form browser by their form name AND any custom name if it exists. * E.g., New Widgets (Smith Interview) */ if (saveAs.getText().toString().trim().equals(mFormDefinition.getName()) || saveAs.getText().toString().trim().length() == 0) saveDataToDisk(EXIT, instanceComplete.isChecked(), null); else saveDataToDisk(EXIT, instanceComplete.isChecked(), saveAs.getText().toString()); // END custom } } }); return endView; case FormEntryController.EVENT_QUESTION: case FormEntryController.EVENT_GROUP: ODKView odkv = null; // should only be a group here if the event_group is a field-list try { odkv = new ODKView(this, mFormController.getQuestionPrompts(), mFormController.getGroupsForCurrentIndex()); Log.i(t, "created view for group"); } catch (RuntimeException e) { createErrorDialog(e.getMessage(), EXIT); e.printStackTrace(); // this is badness to avoid a crash. // really a next view should increment the formcontroller, create the view // if the view is null, then keep the current view and pop an error. return new View(this); } // Makes a "clear answer" menu pop up on long-click for (QuestionWidget qw : odkv.getWidgets()) { if (!qw.getPrompt().isReadOnly()) { registerForContextMenu(qw); } } return odkv; default: Log.e(t, "Attempted to create a view that does not exist."); return null; } } @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() { if (currentPromptIsQuestion()) { if (!saveAnswersForCurrentScreen(EVALUATE_CONSTRAINTS, false)) { // A constraint was violated so a dialog should be showing. return; } } if (mFormController.getEvent() != FormEntryController.EVENT_END_OF_FORM) { int event; group_skip: do { event = mFormController.stepToNextEvent(FormController.STEP_INTO_GROUP); switch (event) { case FormEntryController.EVENT_QUESTION: case FormEntryController.EVENT_END_OF_FORM: View next = createView(event); showView(next, AnimationType.RIGHT); break group_skip; case FormEntryController.EVENT_PROMPT_NEW_REPEAT: createRepeatDialog(); break group_skip; case FormEntryController.EVENT_GROUP: if (mFormController.indexIsInFieldList() && mFormController.getQuestionPrompts().length != 0) { View nextGroupView = createView(event); showView(nextGroupView, AnimationType.RIGHT); break group_skip; } // otherwise it's not a field-list group, so just skip it break; case FormEntryController.EVENT_REPEAT: Log.i(t, "repeat: " + mFormController.getFormIndex().getReference()); // skip repeats break; case FormEntryController.EVENT_REPEAT_JUNCTURE: Log.i(t, "repeat juncture: " + mFormController.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; } } while (event != FormEntryController.EVENT_END_OF_FORM); } else { mBeenSwiped = false; } } /** * 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() { // The answer is saved on a back swipe, but question constraints are ignored. if (currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS, false); } if (mFormController.getEvent() != FormEntryController.EVENT_BEGINNING_OF_FORM) { int event = mFormController.stepToPreviousEvent(); while (event != FormEntryController.EVENT_BEGINNING_OF_FORM && event != FormEntryController.EVENT_QUESTION && !(event == FormEntryController.EVENT_GROUP && mFormController.indexIsInFieldList() && mFormController .getQuestionPrompts().length != 0)) { event = mFormController.stepToPreviousEvent(); } View next = createView(event); showView(next, AnimationType.LEFT); } else { mBeenSwiped = false; } } /** * 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) { switch (from) { case RIGHT: mInAnimation = AnimationUtils.loadAnimation(this, R.anim.push_left_in); mOutAnimation = AnimationUtils.loadAnimation(this, R.anim.push_left_out); break; case LEFT: mInAnimation = AnimationUtils.loadAnimation(this, R.anim.push_right_in); mOutAnimation = AnimationUtils.loadAnimation(this, R.anim.push_right_out); break; case FADE: mInAnimation = AnimationUtils.loadAnimation(this, R.anim.fade_in); mOutAnimation = AnimationUtils.loadAnimation(this, R.anim.fade_out); break; } if (mCurrentView != null) { mCurrentView.startAnimation(mOutAnimation); mRelativeLayout.removeView(mCurrentView); } mInAnimation.setAnimationListener(this); RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); mCurrentView = next; mRelativeLayout.addView(mCurrentView, lp); mCurrentView.startAnimation(mInAnimation); if (mCurrentView instanceof ODKView) ((ODKView) mCurrentView).setFocus(this); else { InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); inputManager.hideSoftInputFromWindow(mCurrentView.getWindowToken(), 0); } } // 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(String constraintText, int saveStatus) { switch (saveStatus) { case FormEntryController.ANSWER_CONSTRAINT_VIOLATED: if (constraintText == null) { constraintText = getString(R.string.invalid_answer_error); } break; case FormEntryController.ANSWER_REQUIRED_BUT_EMPTY: constraintText = getString(R.string.required_answer_error); break; } showCustomToast(constraintText, Toast.LENGTH_SHORT); mBeenSwiped = false; } /** * 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() { 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) { switch (i) { case DialogInterface.BUTTON1: // yes, repeat try { mFormController.newRepeat(); } catch (XPathTypeMismatchException e) { FormEntryActivity.this.createErrorDialog(e.getMessage(), EXIT); return; } showNextView(); break; case DialogInterface.BUTTON2: // no, no repeat showNextView(); break; } } }; if (mFormController.getLastRepeatCount() > 0) { mAlertDialog.setTitle(getString(R.string.leaving_repeat_ask)); mAlertDialog.setMessage(getString(R.string.add_another_repeat, mFormController.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, mFormController.getLastGroupText())); mAlertDialog.setButton(getString(R.string.entering_repeat), repeatListener); mAlertDialog.setButton2(getString(R.string.add_repeat_no), repeatListener); } mAlertDialog.setCancelable(false); mAlertDialog.show(); mBeenSwiped = false; } /** * Creates and displays dialog with the given errorMsg. */ private void createErrorDialog(String errorMsg, final boolean shouldExit) { mErrorMessage = errorMsg; mAlertDialog = new AlertDialog.Builder(this).create(); mAlertDialog.setIcon(android.R.drawable.ic_dialog_alert); 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.BUTTON1: if (shouldExit) { finish(); } break; } } }; mAlertDialog.setCancelable(false); mAlertDialog.setButton(getString(R.string.ok), errorListener); mAlertDialog.show(); } /** * Creates a confirm/cancel dialog for deleting repeats. */ private void createDeleteRepeatConfirmDialog() { mAlertDialog = new AlertDialog.Builder(this).create(); mAlertDialog.setIcon(android.R.drawable.ic_dialog_info); String name = mFormController.getLastRepeatedGroupName(); int repeatcount = mFormController.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) { switch (i) { case DialogInterface.BUTTON1: // yes mFormController.deleteRepeat(); showPreviousView(); break; case DialogInterface.BUTTON2: // no 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 */ private boolean saveDataToDisk(boolean exit, boolean complete, String updatedSaveName) { // save current answer if (!saveAnswersForCurrentScreen(EVALUATE_CONSTRAINTS, !complete)) { Toast.makeText(this, getString(R.string.data_saved_error), Toast.LENGTH_SHORT).show(); return false; } // BEGIN custom // mSaveToDiskTask = // new SaveToDiskTask(getIntent().getData(), exit, complete, updatedSaveName); mSaveToDiskTask = new SaveToDiskTask(getIntent().getData(), exit, complete, updatedSaveName, mFormInstance); // END custom mSaveToDiskTask.setFormSavedListener(this); mSaveToDiskTask.execute(); showDialog(SAVING_DIALOG); return true; } /** * Create a dialog with options to save and exit, save, or quit without saving */ private void createQuitDialog() { String[] items = { getString(R.string.keep_changes), getString(R.string.do_not_save) }; mAlertDialog = new AlertDialog.Builder(this) .setIcon(android.R.drawable.ic_dialog_info) .setTitle(getString(R.string.quit_application, mFormController.getFormTitle())) .setNeutralButton(getString(R.string.do_not_exit), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }).setItems(items, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { switch (which) { case 0: // save and exit saveDataToDisk(EXIT, isInstanceComplete(false), null); break; case 1: // discard changes and exit // BEGIN custom // String selection = // InstanceColumns.INSTANCE_FILE_PATH + " like '" // + mInstancePath + "'"; // Cursor c = // FormEntryActivity.this.managedQuery( // InstanceColumns.CONTENT_URI, null, selection, null, // null); // // // if it's not already saved, erase everything // if (c.getCount() < 1) { // int images = 0; // int audio = 0; // int video = 0; // // delete media first // String instanceFolder = // mInstancePath.substring(0, // mInstancePath.lastIndexOf("/") + 1); // Log.i(t, "attempting to delete: " + instanceFolder); // // String where = // Images.Media.DATA + " like '" + instanceFolder + "%'"; // // String[] projection = { // Images.ImageColumns._ID // }; // // // images // Cursor imageCursor = // getContentResolver() // .query( // android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, // projection, where, null, null); // if (imageCursor.getCount() > 0) { // imageCursor.moveToFirst(); // String id = // imageCursor.getString(imageCursor // .getColumnIndex(Images.ImageColumns._ID)); // // Log.i( // t, // "attempting to delete: " // + Uri.withAppendedPath( // android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, // id)); // images = // getContentResolver() // .delete( // Uri.withAppendedPath( // android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, // id), null, null); // } // imageCursor.close(); // // // audio // Cursor audioCursor = // getContentResolver().query( // MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, // projection, where, null, null); // if (audioCursor.getCount() > 0) { // audioCursor.moveToFirst(); // String id = // audioCursor.getString(imageCursor // .getColumnIndex(Images.ImageColumns._ID)); // // Log.i( // t, // "attempting to delete: " // + Uri.withAppendedPath( // MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, // id)); // audio = // getContentResolver() // .delete( // Uri.withAppendedPath( // MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, // id), null, null); // } // audioCursor.close(); // // // video // Cursor videoCursor = // getContentResolver().query( // MediaStore.Video.Media.EXTERNAL_CONTENT_URI, // projection, where, null, null); // if (videoCursor.getCount() > 0) { // videoCursor.moveToFirst(); // String id = // videoCursor.getString(imageCursor // .getColumnIndex(Images.ImageColumns._ID)); // // Log.i( // t, // "attempting to delete: " // + Uri.withAppendedPath( // MediaStore.Video.Media.EXTERNAL_CONTENT_URI, // id)); // video = // getContentResolver() // .delete( // Uri.withAppendedPath( // MediaStore.Video.Media.EXTERNAL_CONTENT_URI, // id), null, null); // } // audioCursor.close(); // // 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(); // } // } // // finishReturnInstance(); tidyBeforeFinish(); // END custom finishReturnInstance(); break; case 2:// do nothing break; } } }).create(); mAlertDialog.show(); } /** * Confirm clear answer dialog */ private void createClearDialog(final QuestionWidget qw) { 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.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.BUTTON1: // yes clearAnswer(qw); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS, false); break; case DialogInterface.BUTTON2: // no 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() { final String[] languages = mFormController.getLanguages(); int selected = -1; if (languages != null) { String language = mFormController.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) { // BEGIN custom /* Disabled this functionality until we can integrate in into our world (new issue) */ // // 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"); // END custom mFormController.setLanguage(languages[whichButton]); dialog.dismiss(); if (currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS, false); } 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) { } }).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: mProgressDialog = new ProgressDialog(this); DialogInterface.OnClickListener loadingButtonListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); mFormLoaderTask.setFormLoaderListener(null); mFormLoaderTask.cancel(true); 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: mProgressDialog = new ProgressDialog(this); DialogInterface.OnClickListener savingButtonListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); mSaveToDiskTask.setFormSavedListener(null); mSaveToDiskTask.cancel(true); } }; 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.setCancelable(false); mProgressDialog.setButton(getString(R.string.cancel), savingButtonListener); mProgressDialog.setButton(getString(R.string.cancel_saving_form), savingButtonListener); return mProgressDialog; // BEGIN custom case REMOVE_DIALOG: AlertDialog.Builder builder = new AlertDialog.Builder(this); Dialog dialog = null; String message; if (isInstanceComplete(false)) message = getString(R.string.tf_confirm_complete_instance_removal_dialog_msg); else message = getString(R.string.tf_confirm_instance_removal_dialog_msg); builder .setCancelable(false) .setIcon(R.drawable.ic_dialog_info) .setMessage(message); builder.setPositiveButton(getString(R.string.tf_remove), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { try { mFormInstance.setStatus(FormInstance.Status.removed); Collect.getInstance().getDbService().getDb().update(mFormInstance); removeDialog(REMOVE_DIALOG); Toast.makeText(getApplicationContext(), getString(R.string.tf_removed_with_param, mFormDefinition.getName()), Toast.LENGTH_SHORT).show(); if (mInstances.size() > 1) { browseToNextInstance(true); } else { finish(); } } catch (Exception e) { if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + "problem marking form instance document as removed " + e.toString()); Toast.makeText(getApplicationContext(), getString(R.string.tf_unable_to_remove_form_instance), Toast.LENGTH_LONG).show(); } } }); builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { removeDialog(REMOVE_DIALOG); } }); dialog = builder.create(); return dialog; case INFO_DIALOG: InstanceInfoDialog fii = new InstanceInfoDialog(this, mFormDefinition, mFormInstance); fii.setOnDismissListener(new OnDismissListener () { @Override public void onDismiss(DialogInterface dialog) { removeDialog(INFO_DIALOG); } }); return fii; // END custom } return null; } /** * Dismiss any showing dialogs that we manually manage. */ private void dismissDialogs() { if (mAlertDialog != null && mAlertDialog.isShowing()) { mAlertDialog.dismiss(); } } @Override protected void onPause() { dismissDialogs(); if (mCurrentView != null && currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS, false); } super.onPause(); } @Override protected void onResume() { super.onResume(); if (mFormLoaderTask != null) { mFormLoaderTask.setFormLoaderListener(this); if (mFormController != null && mFormLoaderTask.getStatus() == AsyncTask.Status.FINISHED) { dismissDialog(PROGRESS_DIALOG); refreshCurrentView(); } } if (mSaveToDiskTask != null) { mSaveToDiskTask.setFormSavedListener(this); } if (mErrorMessage != null && (mAlertDialog != null && !mAlertDialog.isShowing())) { createErrorDialog(mErrorMessage, EXIT); return; } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_BACK: createQuitDialog(); return true; case KeyEvent.KEYCODE_DPAD_RIGHT: if (event.isAltPressed() && !mBeenSwiped) { mBeenSwiped = true; showNextView(); return true; } break; case KeyEvent.KEYCODE_DPAD_LEFT: if (event.isAltPressed() && !mBeenSwiped) { mBeenSwiped = true; 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) { mFormLoaderTask.cancel(true); mFormLoaderTask.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(false); } } super.onDestroy(); } @Override public void onAnimationEnd(Animation arg0) { mBeenSwiped = false; } @Override public void onAnimationRepeat(Animation animation) { // Added by AnimationListener interface. } @Override public void onAnimationStart(Animation animation) { // Added by AnimationListener interface. } /** * loadingComplete() is called by FormLoaderTask once it has finished loading a form. */ @Override // BEGIN custom // public void loadingComplete(FormController fc) { public void loadingComplete(FormController fc, FormDefinition fdd, FormInstance fid) { final String tt = t + ": loadingComplete(): "; // END custom dismissDialog(PROGRESS_DIALOG); mFormController = fc; // BEGIN custom mFormDefinition = fdd; mFormInstance = fid; // END custom // Set saved answer path if (mInstancePath == null) { // BEGIN custom // // Create new answer folder. // String time = // new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss") // .format(Calendar.getInstance().getTime()); // String file = // mFormPath.substring(mFormPath.lastIndexOf('/') + 1, mFormPath.lastIndexOf('.')); // String path = Collect.INSTANCES_PATH + "/" + file + "_" + time; // if (FileUtils.createFolder(path)) { // mInstancePath = path + "/" + file + "_" + time + ".xml"; // } // Create temporary instance & folder try { fid = new FormInstance(); fid.setFormId(fdd.getId()); fid.setStatus(FormInstance.Status.placeholder); Collect.getInstance().getDbService().getDb().create(fid); String instanceFolder = FileUtilsExtended.INSTANCES_PATH + File.separator + fid.getId(); FileUtils.createFolder(instanceFolder); mInstancePath = instanceFolder + File.separator + fid.getId() + ".xml"; mFormInstance = fid; } catch (Exception e) { if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, tt + "failed to create temporary instance and/or folder"); e.printStackTrace(); createErrorDialog("Unable to create new form placeholder and/or folder. Please report this problem and try again.", EXIT); } // END custom } else { // we've just loaded a saved form, so start in the hierarchy view Intent i = new Intent(this, FormHierarchyActivity.class); // BEGIN custom // startActivity(i); i.putExtra(FormHierarchyActivity.KEY_AUTOLOAD, true); i.putStringArrayListExtra(KEY_INSTANCES, mInstances); startActivityForResult(i, HIERARCHY_ACTIVITY); // END custom return; // so we don't show the intro screen before jumping to the hierarchy } // BEGIN custom /* Disabled this functionality until we can integrate in into our world (new issue) */ // // Set the language if one has already been set in the past // String[] languageTest = mFormController.getLanguages(); // if (languageTest != null) { // String defaultLanguage = mFormController.getLanguage(); // String newLanguage = ""; // String selection = FormsColumns.FORM_FILE_PATH + "=?"; // String selectArgs[] = { // mFormPath // }; // Cursor c = managedQuery(FormsColumns.CONTENT_URI, null, selection, selectArgs, null); // if (c.getCount() == 1) { // c.moveToFirst(); // newLanguage = c.getString(c.getColumnIndex(FormsColumns.LANGUAGE)); // } // // // if somehow we end up with a bad language, set it to the default // try { // mFormController.setLanguage(newLanguage); // } catch (Exception e) { // mFormController.setLanguage(defaultLanguage); // } // } // END custom 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 // BEGIN custom // public void savingComplete(int saveStatus) { public void savingComplete(int saveStatus, FormInstance fi) { // END custom dismissDialog(SAVING_DIALOG); switch (saveStatus) { case SaveToDiskTask.SAVED: Toast.makeText(this, getString(R.string.data_saved_ok), Toast.LENGTH_SHORT).show(); // BEGIN custom mFormInstance = fi; // END custom break; case SaveToDiskTask.SAVED_AND_EXIT: Toast.makeText(this, getString(R.string.data_saved_ok), Toast.LENGTH_SHORT).show(); // BEGIN custom tidyBeforeFinish(); // END custom finishReturnInstance(); break; case SaveToDiskTask.SAVE_ERROR: Toast.makeText(this, getString(R.string.data_saved_error), Toast.LENGTH_LONG) .show(); break; case FormEntryController.ANSWER_CONSTRAINT_VIOLATED: case FormEntryController.ANSWER_REQUIRED_BUT_EMPTY: refreshCurrentView(); // an answer constraint was violated, so do a 'swipe' to the next // question to display the proper toast(s) next(); break; } } /** * 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, boolean ignoreRequiredConstraint) { if (evaluateConstraints) { int saveStatus = mFormController.answerQuestion(index, answer); if (ignoreRequiredConstraint && saveStatus == FormEntryController.ANSWER_REQUIRED_BUT_EMPTY) { mFormController.saveAnswer(index, answer); return FormEntryController.ANSWER_OK; } return saveStatus; } else { mFormController.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) { // 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); // BEGIN custom // complete = // sharedPreferences.getBoolean(PreferencesActivity.KEY_COMPLETED_DEFAULT, true); complete = sharedPreferences.getBoolean(com.radicaldynamic.gcmobile.android.preferences.PreferencesActivity.KEY_COMPLETE_BY_DEFAULT, false); } // // Then see if we've already marked this form as complete before // String selection = InstanceColumns.INSTANCE_FILE_PATH + "=?"; // String[] selectionArgs = { // mInstancePath // }; // Cursor c = // getContentResolver().query(InstanceColumns.CONTENT_URI, null, selection, selectionArgs, // null); // startManagingCursor(c); // 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; // } // } // return complete; if (mFormInstance.getStatus().equals(FormInstance.Status.draft) || mFormInstance.getStatus().equals(FormInstance.Status.placeholder)) return complete; else return mFormInstance.getStatus().equals(FormInstance.Status.complete); // END custom } 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() { String action = getIntent().getAction(); if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_EDIT.equals(action)) { // BEGIN custom // // caller is waiting on a picked form // String selection = InstanceColumns.INSTANCE_FILE_PATH + "=?"; // String[] selectionArgs = { // mInstancePath // }; // Cursor c = // managedQuery(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)); // } setResult(RESULT_OK, new Intent().putExtra(KEY_INSTANCEPATH, mFormInstance.getId())); // END custom } finish(); } @Override public boolean onDown(MotionEvent e) { return false; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // 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 ((Math.abs(e1.getX() - e2.getX()) > xPixelLimit && Math.abs(e1.getY() - e2.getY()) < yPixelLimit) || Math.abs(e1.getX() - e2.getX()) > xPixelLimit * 2) { if (velocityX > 0) { mBeenSwiped = true; showPreviousView(); return true; } else { mBeenSwiped = true; 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. mCurrentView.cancelLongPress(); return false; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public void advance() { next(); } // BEGIN custom private void browseToInstance(String instanceId) { if (Collect.Log.VERBOSE) Log.v(Collect.LOGTAG, t + ": about to browse to " + instanceId + " using form " + mFormDefinition.getId() + " with list " + mInstances.toString()); tidyBeforeFinish(); finish(); Intent i = new Intent(this, FormEntryActivity.class); i.putStringArrayListExtra(FormEntryActivity.KEY_INSTANCES, mInstances); i.putExtra(FormEntryActivity.KEY_FORMPATH, mFormDefinition.getId()); i.putExtra(FormEntryActivity.KEY_INSTANCEPATH, instanceId); startActivity(i); } private void browseToNextInstance(boolean removeCurrentInstance) { String nextInstanceId; if (mInstances.indexOf(mFormInstance.getId()) < mInstances.size() - 1) { nextInstanceId = mInstances.listIterator(mInstances.indexOf(mFormInstance.getId()) + 1).next(); } else { nextInstanceId = mInstances.get(0); } // For when a user "removes" a form instance if (removeCurrentInstance) mInstances.remove(mFormInstance.getId()); browseToInstance(nextInstanceId); } private void browseToPreviousInstance() { String previousInstanceId; if (mInstances.listIterator(mInstances.indexOf(mFormInstance.getId())).hasPrevious()) { previousInstanceId = mInstances.listIterator(mInstances.indexOf(mFormInstance.getId())).previous(); } else { previousInstanceId = mInstances. get(mInstances.size() - 1); } browseToInstance(previousInstanceId); } private void tidyBeforeFinish() { // Check to see if we need to remove the placeholder document if (mFormInstance != null && mFormInstance.getStatus() == FormInstance.Status.placeholder) { try { mFormInstance = Collect.getInstance().getDbService().getDb().get(FormInstance.class, mFormInstance.getId()); if (mFormInstance.getStatus() == FormInstance.Status.placeholder) { if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + ": removing placeholder " + mFormInstance.getId()); Collect.getInstance().getDbService().getDb().delete(mFormInstance); } } catch (Exception e) { if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + ": unexpected exception while running tidyBeforeFinish()"); e.printStackTrace(); } } // Remove the instance directory, if any if (mInstancePath != null) { String instanceDir = mInstancePath.substring(0, mInstancePath.lastIndexOf("/")); if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + ": removing instance directory " + instanceDir); FileUtilsExtended.deleteFolder(instanceDir); } } // END custom }