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 com.kll.collect.android.activities; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileFilter; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.LinkedHashMap; import java.util.Locale; import org.apache.commons.io.IOUtils; import org.apache.http.entity.mime.content.ContentBody; import org.apache.http.entity.mime.content.InputStreamBody; import org.javarosa.core.model.FormIndex; import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.services.transport.payload.ByteArrayPayload; import org.javarosa.form.api.FormEntryCaption; import org.javarosa.form.api.FormEntryController; import org.javarosa.form.api.FormEntryPrompt; import org.javarosa.model.xform.XFormsModule; import org.javarosa.xform.parse.XFormParser; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import com.kll.collect.android.R; import com.kll.collect.android.application.Collect; import com.kll.collect.android.exception.JavaRosaException; import com.kll.collect.android.listeners.AdvanceToNextListener; import com.kll.collect.android.listeners.FormLoaderListener; import com.kll.collect.android.listeners.FormSavedListener; import com.kll.collect.android.listeners.SavePointListener; import com.kll.collect.android.logic.FormController; import com.kll.collect.android.logic.PropertyManager; import com.kll.collect.android.logic.FormController.FailedConstraint; import com.kll.collect.android.preferences.AdminPreferencesActivity; import com.kll.collect.android.preferences.PreferencesActivity; import com.kll.collect.android.provider.InstanceProviderAPI; import com.kll.collect.android.provider.FormsProviderAPI.FormsColumns; import com.kll.collect.android.provider.InstanceProviderAPI.InstanceColumns; import com.kll.collect.android.tasks.FormLoaderTask; import com.kll.collect.android.tasks.SavePointTask; import com.kll.collect.android.tasks.SaveResult; import com.kll.collect.android.tasks.SaveToDiskTask; import com.kll.collect.android.utilities.CompatibilityUtils; import com.kll.collect.android.utilities.FileUtils; import com.kll.collect.android.utilities.InfoLogger; import com.kll.collect.android.utilities.MediaUtils; import com.kll.collect.android.views.ODKView; import com.kll.collect.android.widgets.GeoPointWidget; import com.kll.collect.android.widgets.QuestionWidget; import com.kll.collect.android.utilities.ScalingUtilities; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.location.LocationProvider; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.preference.PreferenceManager; import android.provider.MediaStore.Images; import android.text.InputFilter; import android.text.Spanned; import android.util.DisplayMetrics; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.GestureDetector; import android.view.GestureDetector.OnGestureListener; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup.LayoutParams; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.AnimationUtils; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; /** * FormEntryActivity is responsible for displaying questions, animating * transitions between questions, and allowing the user to enter data. * * @author Carl Hartung (carlhartung@gmail.com) * @author Thomas Smyth, Sassafras Tech Collective (tom@sassafrastech.com; constraint behavior option) */ public class FormEntryActivity extends Activity implements AnimationListener, FormLoaderListener, FormSavedListener, AdvanceToNextListener, OnGestureListener, SavePointListener, LocationListener { private static final String t = "FormEntryActivity"; // save with every swipe forward or back. Timings indicate this takes .25 // seconds. // if it ever becomes an issue, this value can be changed to save every n'th // screen. private static final int SAVEPOINT_INTERVAL = 1; // Defines for FormEntryActivity private static final boolean EXIT = true; private static final boolean DO_NOT_EXIT = false; private static final boolean EVALUATE_CONSTRAINTS = true; private static final boolean DO_NOT_EVALUATE_CONSTRAINTS = false; // Request codes for returning data from specified intent. public static final int IMAGE_CAPTURE = 1; public static final int BARCODE_CAPTURE = 2; public static final int AUDIO_CAPTURE = 3; public static final int VIDEO_CAPTURE = 4; public static final int LOCATION_CAPTURE = 5; public static final int HIERARCHY_ACTIVITY = 6; public static final int IMAGE_CHOOSER = 7; public static final int AUDIO_CHOOSER = 8; public static final int VIDEO_CHOOSER = 9; public static final int EX_STRING_CAPTURE = 10; public static final int EX_INT_CAPTURE = 11; public static final int EX_DECIMAL_CAPTURE = 12; public static final int DRAW_IMAGE = 13; public static final int SIGNATURE_CAPTURE = 14; public static final int ANNOTATE_IMAGE = 15; public static final int ALIGNED_IMAGE = 16; public static final int BEARING_CAPTURE = 17; public static final int EX_GROUP_CAPTURE = 18; public static final int GEOSHAPE_CAPTURE = 19; public static final int GEOTRACE_CAPTURE = 20; // 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 LOCATION_SHAPE_RESULT = "LOCATION_SHAPE_RESULT"; public static final String KEY_INSTANCES = "instances"; public static final String KEY_SUCCESS = "success"; public static final String KEY_ERROR = "error"; // Identifies the gp of the form used to launch form entry public static final String KEY_FORMPATH = "formpath"; // Identifies whether this is a new form, or reloading a form after a screen // rotation (or similar) private static final String NEWFORM = "newform"; // these are only processed if we shut down and are restoring after an // external intent fires public static final String KEY_INSTANCEPATH = "instancepath"; public static final String KEY_XPATH = "xpath"; public static final String KEY_XPATH_WAITING_FOR_DATA = "xpathwaiting"; 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 NEXT = 1; private static final int PREVIOUS = 2; // Random ID private static final int DELETE_REPEAT = 654321; private String mFormPath; private GestureDetector mGestureDetector; private Animation mInAnimation; private Animation mOutAnimation; private View mStaleView = null; //private ProgressBar progressBar; private LinearLayout mQuestionHolder; private View mCurrentView; private AlertDialog mAlertDialog; private ProgressDialog mProgressDialog; private String mErrorMessage; // used to limit forward/backward swipes to one per question private boolean mBeenSwiped = false; private final Object saveDialogLock = new Object(); private int viewCount = 0; private FormLoaderTask mFormLoaderTask; private SaveToDiskTask mSaveToDiskTask; private ImageButton mNextButton; private ImageButton mBackButton; private String stepMessage = ""; private double progressValue = 0.0; private String FIELD_LIST = "field-list"; enum AnimationType { LEFT, RIGHT, FADE } public static LocationListener mLocationListner; private static LocationManager mLocationManager; private Location mLocation; private boolean mGPSOn = false; private boolean mNetworkOn = false; private double mLocationAccuracy; private int mLocationCount = 0; public boolean needLocation = false; private boolean compressImage = false; private SharedPreferences mAdminPreferences; private SharedPreferences mSharedPreferences; /** 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; } Log.i("Activity", "Created"); mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); mSharedPreferences .registerOnSharedPreferenceChangeListener(new SharedPreferences.OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { compressImage = mSharedPreferences .getBoolean(PreferencesActivity.KEY_ENABLE_IMAGE_COMPRESSION, false); } }); setContentView(R.layout.form_entry); /* setTitle(getString(R.string.app_name) + " > " + getString(R.string.loading_form));*/ setTitle(getString(R.string.app_name)); Log.i("Entry", "Form"); mErrorMessage = null; //progressBar = (ProgressBar) findViewById(R.id.progress); //progressBar.setVisibility(ProgressBar.VISIBLE); //progressBar.setProgress(0); mBeenSwiped = false; mAlertDialog = null; mCurrentView = null; mInAnimation = null; mOutAnimation = null; mGestureDetector = new GestureDetector(this, this); mQuestionHolder = (LinearLayout) findViewById(R.id.questionholder); // get admin preference settings mAdminPreferences = getSharedPreferences(AdminPreferencesActivity.ADMIN_PREFERENCES, 0); mNextButton = (ImageButton) findViewById(R.id.form_forward_button); mNextButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mBeenSwiped = true; showNextView(); } }); mBackButton = (ImageButton) findViewById(R.id.form_back_button); mBackButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mBeenSwiped = true; showPreviousView(); } }); needLocation = mSharedPreferences.getBoolean(PreferencesActivity.KEY_GPS_FIX, false); if (needLocation) { mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 5000, 5, 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())); String startingXPath = null; String waitingXPath = null; String instancePath = null; Boolean newForm = true; if (savedInstanceState != null) { if (savedInstanceState.containsKey(KEY_FORMPATH)) { mFormPath = savedInstanceState.getString(KEY_FORMPATH); } if (savedInstanceState.containsKey(KEY_INSTANCEPATH)) { instancePath = savedInstanceState.getString(KEY_INSTANCEPATH); } if (savedInstanceState.containsKey(KEY_XPATH)) { startingXPath = savedInstanceState.getString(KEY_XPATH); Log.i(t, "startingXPath is: " + startingXPath); } if (savedInstanceState.containsKey(KEY_XPATH_WAITING_FOR_DATA)) { waitingXPath = savedInstanceState.getString(KEY_XPATH_WAITING_FOR_DATA); Log.i(t, "waitingXPath is: " + waitingXPath); } if (savedInstanceState.containsKey(NEWFORM)) { newForm = savedInstanceState.getBoolean(NEWFORM, true); } if (savedInstanceState.containsKey(KEY_ERROR)) { mErrorMessage = savedInstanceState.getString(KEY_ERROR); } } // If a parse error message is showing then nothing else is loaded // Dialogs mid form just disappear on rotation. if (mErrorMessage != null) { createErrorDialog(mErrorMessage, EXIT); return; } // Check to see if this is a screen flip or a new form load. Object data = getLastNonConfigurationInstance(); if (data instanceof FormLoaderTask) { mFormLoaderTask = (FormLoaderTask) data; } else if (data instanceof SaveToDiskTask) { mSaveToDiskTask = (SaveToDiskTask) data; } else if (data == null) { if (!newForm) { if (Collect.getInstance().getFormController() != null) { refreshCurrentView(); } else { Log.w(t, "Reloading form and restoring state."); // we need to launch the form loader to load the form // controller... mFormLoaderTask = new FormLoaderTask(instancePath, startingXPath, waitingXPath); Collect.getInstance().getActivityLogger().logAction(this, "formReloaded", mFormPath); // TODO: this doesn' work (dialog does not get removed): // showDialog(PROGRESS_DIALOG); // show dialog before we execute... Log.i("Loader", "Executing"); mFormLoaderTask.execute(mFormPath); } return; } // Not a restart from a screen orientation change (or other). Collect.getInstance().setFormController(null); CompatibilityUtils.invalidateOptionsMenu(this); Intent intent = getIntent(); if (intent != null) { Uri uri = intent.getData(); if (getContentResolver().getType(uri).equals(InstanceColumns.CONTENT_ITEM_TYPE)) { // get the formId and version for this instance... String jrFormId = null; String jrVersion = null; { Cursor instanceCursor = null; try { instanceCursor = getContentResolver().query(uri, null, null, null, null); if (instanceCursor.getCount() != 1) { this.createErrorDialog("Bad URI: " + uri, EXIT); return; } else { instanceCursor.moveToFirst(); instancePath = instanceCursor.getString( instanceCursor.getColumnIndex(InstanceColumns.INSTANCE_FILE_PATH)); Collect.getInstance().getActivityLogger().logAction(this, "instanceLoaded", instancePath); jrFormId = instanceCursor .getString(instanceCursor.getColumnIndex(InstanceColumns.JR_FORM_ID)); int idxJrVersion = instanceCursor.getColumnIndex(InstanceColumns.JR_VERSION); jrVersion = instanceCursor.isNull(idxJrVersion) ? null : instanceCursor.getString(idxJrVersion); } } finally { if (instanceCursor != null) { instanceCursor.close(); } } } String[] selectionArgs; String selection; if (jrVersion == null) { selectionArgs = new String[] { jrFormId }; selection = FormsColumns.JR_FORM_ID + "=? AND " + FormsColumns.JR_VERSION + " IS NULL"; } else { selectionArgs = new String[] { jrFormId, jrVersion }; selection = FormsColumns.JR_FORM_ID + "=? AND " + FormsColumns.JR_VERSION + "=?"; } { Cursor formCursor = null; try { formCursor = getContentResolver().query(FormsColumns.CONTENT_URI, null, selection, selectionArgs, null); if (formCursor.getCount() == 1) { formCursor.moveToFirst(); mFormPath = formCursor .getString(formCursor.getColumnIndex(FormsColumns.FORM_FILE_PATH)); } else if (formCursor.getCount() < 1) { this.createErrorDialog( getString(R.string.parent_form_not_present, jrFormId) + ((jrVersion == null) ? "" : "\n" + getString(R.string.version) + " " + jrVersion), EXIT); return; } else if (formCursor.getCount() > 1) { // still take the first entry, but warn that // there are multiple rows. // user will need to hand-edit the SQLite // database to fix it. formCursor.moveToFirst(); mFormPath = formCursor .getString(formCursor.getColumnIndex(FormsColumns.FORM_FILE_PATH)); this.createErrorDialog(getString(R.string.survey_multiple_forms_error), EXIT); return; } } finally { if (formCursor != null) { formCursor.close(); } } } } else if (getContentResolver().getType(uri).equals(FormsColumns.CONTENT_ITEM_TYPE)) { Cursor c = null; try { c = getContentResolver().query(uri, null, null, null, null); if (c.getCount() != 1) { this.createErrorDialog("Bad URI: " + uri, EXIT); return; } else { c.moveToFirst(); mFormPath = c.getString(c.getColumnIndex(FormsColumns.FORM_FILE_PATH)); // This is the fill-blank-form code path. // See if there is a savepoint for this form that // has never been // explicitly saved // by the user. If there is, open this savepoint // (resume this filled-in // form). // Savepoints for forms that were explicitly saved // will be recovered // when that // explicitly saved instance is edited via // edit-saved-form. final String filePrefix = mFormPath.substring(mFormPath.lastIndexOf('/') + 1, mFormPath.lastIndexOf('.')) + "_"; final String fileSuffix = ".xml.save"; File cacheDir = new File(Collect.CACHE_PATH); File[] files = cacheDir.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { String name = pathname.getName(); return name.startsWith(filePrefix) && name.endsWith(fileSuffix); } }); // see if any of these savepoints are for a // filled-in form that has never been // explicitly saved by the user... for (int i = 0; i < files.length; ++i) { File candidate = files[i]; String instanceDirName = candidate.getName().substring(0, candidate.getName().length() - fileSuffix.length()); File instanceDir = new File( Collect.INSTANCES_PATH + File.separator + instanceDirName); File instanceFile = new File(instanceDir, instanceDirName + ".xml"); if (instanceDir.exists() && instanceDir.isDirectory() && !instanceFile.exists()) { // yes! -- use this savepoint file instancePath = instanceFile.getAbsolutePath(); break; } } } } finally { if (c != null) { c.close(); } } } else { Log.e(t, "unrecognized URI"); this.createErrorDialog("Unrecognized URI: " + uri, EXIT); return; } mFormLoaderTask = new FormLoaderTask(instancePath, null, null); Collect.getInstance().getActivityLogger().logAction(this, "formLoaded", mFormPath); showDialog(PROGRESS_DIALOG); // show dialog before we execute... Log.i("Loader", "Executing"); mFormLoaderTask.execute(mFormPath); } } } /** * Create save-points asynchronously in order to not affect swiping performance * on larger forms. */ private void nonblockingCreateSavePointData() { try { SavePointTask savePointTask = new SavePointTask(this); savePointTask.execute(); } catch (Exception e) { Log.e(t, "Could not schedule SavePointTask. Perhaps a lot of swiping is taking place?"); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(KEY_FORMPATH, mFormPath); FormController formController = Collect.getInstance().getFormController(); if (formController != null) { outState.putString(KEY_INSTANCEPATH, formController.getInstancePath().getAbsolutePath()); outState.putString(KEY_XPATH, formController.getXPath(formController.getFormIndex())); FormIndex waiting = formController.getIndexWaitingForData(); if (waiting != null) { outState.putString(KEY_XPATH_WAITING_FOR_DATA, formController.getXPath(waiting)); } // save the instance to a temp path... nonblockingCreateSavePointData(); } outState.putBoolean(NEWFORM, false); outState.putString(KEY_ERROR, mErrorMessage); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); Log.i("Request Code", Integer.toString(requestCode)); Log.i("Result Code", Integer.toString(resultCode)); FormController formController = Collect.getInstance().getFormController(); if (formController == null) { // we must be in the midst of a reload of the FormController. // try to save this callback data to the FormLoaderTask if (mFormLoaderTask != null && mFormLoaderTask.getStatus() != AsyncTask.Status.FINISHED) { mFormLoaderTask.setActivityResult(requestCode, resultCode, intent); } else { Log.e(t, "Got an activityResult without any pending form loader"); } return; } compressImage = mSharedPreferences.getBoolean(PreferencesActivity.KEY_ENABLE_IMAGE_COMPRESSION, false); if (resultCode == RESULT_CANCELED) { // request was canceled... if (requestCode != HIERARCHY_ACTIVITY) { ((ODKView) mCurrentView).cancelWaitingForBinaryData(); } return; } Log.i("Request Code ", Integer.toString(requestCode)); switch (requestCode) { case BARCODE_CAPTURE: String sb = intent.getStringExtra("SCAN_RESULT"); ((ODKView) mCurrentView).setBinaryData(sb); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case EX_STRING_CAPTURE: case EX_INT_CAPTURE: case EX_DECIMAL_CAPTURE: String key = "value"; boolean exists = intent.getExtras().containsKey(key); if (exists) { Object externalValue = intent.getExtras().get(key); ((ODKView) mCurrentView).setBinaryData(externalValue); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } break; case EX_GROUP_CAPTURE: try { Bundle extras = intent.getExtras(); ((ODKView) mCurrentView).setDataForFields(extras); } catch (JavaRosaException e) { Log.e(t, e.getMessage(), e); createErrorDialog(e.getCause().getMessage(), DO_NOT_EXIT); } break; case DRAW_IMAGE: case ANNOTATE_IMAGE: case SIGNATURE_CAPTURE: case IMAGE_CAPTURE: /* * We saved the image to the tempfile_path, but we really want it to * be in: /sdcard/odk/instances/[current instnace]/something.jpg so * we move it there before inserting it into the content provider. * Once the android image capture bug gets fixed, (read, we move on * from Android 1.6) we want to handle images the audio and video */ // The intent is empty, but we know we saved the image to the temp // file Log.i("Request Code ", Integer.toString(requestCode)); File fi = new File(Collect.TMPFILE_PATH); String mInstanceFolder = formController.getInstancePath().getParent(); String s = mInstanceFolder + File.separator + System.currentTimeMillis() + ".jpg"; File nf = new File(s); if (!fi.renameTo(nf)) { Log.e(t, "Failed to rename " + fi.getAbsolutePath()); } else { Log.i(t, "renamed " + fi.getAbsolutePath() + " to " + nf.getAbsolutePath()); } Log.i("Filename of image", String.valueOf(nf)); if (compressImage) compreesImage(s); ((ODKView) mCurrentView).setBinaryData(nf); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case ALIGNED_IMAGE: /* * We saved the image to the tempfile_path; the app returns the full * path to the saved file in the EXTRA_OUTPUT extra. Take that file * and move it into the instance folder. */ String path = intent.getStringExtra(android.provider.MediaStore.EXTRA_OUTPUT); fi = new File(path); mInstanceFolder = formController.getInstancePath().getParent(); s = mInstanceFolder + File.separator + System.currentTimeMillis() + ".jpg"; nf = new File(s); if (!fi.renameTo(nf)) { Log.e(t, "Failed to rename " + fi.getAbsolutePath()); } else { Log.i(t, "renamed " + fi.getAbsolutePath() + " to " + nf.getAbsolutePath()); } if (compressImage) compreesImage(s); ((ODKView) mCurrentView).setBinaryData(nf); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case IMAGE_CHOOSER: /* * We have a saved image somewhere, but we really want it to be in: * /sdcard/odk/instances/[current instnace]/something.jpg so we move * it there before inserting it into the content provider. Once the * android image capture bug gets fixed, (read, we move on from * Android 1.6) we want to handle images the audio and video */ // get gp of chosen file Uri selectedImage = intent.getData(); String sourceImagePath = MediaUtils.getPathFromUri(this, selectedImage, Images.Media.DATA); // Copy file to sdcard String mInstanceFolder1 = formController.getInstancePath().getParent(); String destImagePath = mInstanceFolder1 + File.separator + System.currentTimeMillis() + ".jpg"; File source = new File(sourceImagePath); File newImage = new File(destImagePath); FileUtils.copyFile(source, newImage); if (compressImage) compreesImage(destImagePath); ((ODKView) mCurrentView).setBinaryData(newImage); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case AUDIO_CAPTURE: case VIDEO_CAPTURE: case AUDIO_CHOOSER: case VIDEO_CHOOSER: // For audio/video capture/chooser, we get the URI from the content // provider // then the widget copies the file and makes a new entry in the // content provider. Uri media = intent.getData(); ((ODKView) mCurrentView).setBinaryData(media); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case LOCATION_CAPTURE: Log.i("Request Code ", Integer.toString(requestCode)); String sl = intent.getStringExtra(LOCATION_RESULT); ((ODKView) mCurrentView).setBinaryData(sl); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case GEOSHAPE_CAPTURE: //String ls = intent.getStringExtra(GEOSHAPE_RESULTS); String gshr = intent.getStringExtra(GEOSHAPE_RESULTS); ((ODKView) mCurrentView).setBinaryData(gshr); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case GEOTRACE_CAPTURE: String traceExtra = intent.getStringExtra(GEOTRACE_RESULTS); ((ODKView) mCurrentView).setBinaryData(traceExtra); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case BEARING_CAPTURE: String bearing = intent.getStringExtra(BEARING_RESULT); ((ODKView) mCurrentView).setBinaryData(bearing); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); case HIERARCHY_ACTIVITY: // We may have jumped to a new index in hierarchy activity, so // refresh break; } refreshCurrentView(); } private void compreesImage(String path) { Bitmap scaledBitmap = null; try { // Part 1: Decode image Bitmap unscaledBitmap = ScalingUtilities.decodeFile(path, ScalingUtilities.DESIREDWIDTH, ScalingUtilities.DESIREDHEIGHT, ScalingUtilities.ScalingLogic.FIT); if (!(unscaledBitmap.getWidth() <= 800 && unscaledBitmap.getHeight() <= 800)) { // Part 2: Scale image scaledBitmap = ScalingUtilities.createScaledBitmap(unscaledBitmap, ScalingUtilities.DESIREDWIDTH, ScalingUtilities.DESIREDHEIGHT, ScalingUtilities.ScalingLogic.FIT); } else { unscaledBitmap.recycle(); } // Store to tmp file File f = new File(path); FileOutputStream fos = null; try { fos = new FileOutputStream(f); scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos); fos.flush(); fos.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } scaledBitmap.recycle(); } catch (Throwable e) { } } /** * Refreshes the current view. the controller and the displayed view can get * out of sync due to dialogs and restarts caused by screen orientation * changes, so they're resynchronized here. */ public void refreshCurrentView() { FormController formController = Collect.getInstance().getFormController(); int event = formController.getEvent(); // When we refresh, repeat dialog state isn't maintained, so step back // to the previous // question. // Also, if we're within a group labeled 'field list', step back to the // beginning of that // group. // That is, skip backwards over repeat prompts, groups that are not // field-lists, // repeat events, and indexes in field-lists that is not the containing // group. if (event == FormEntryController.EVENT_PROMPT_NEW_REPEAT) { createRepeatDialog(); } else { View current = createView(event, false, 0); showView(current, AnimationType.FADE); } } @Override public boolean onCreateOptionsMenu(Menu menu) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateOptionsMenu", "show"); super.onCreateOptionsMenu(menu); CompatibilityUtils.setShowAsAction( menu.add(0, MENU_SAVE, 0, R.string.save_all_answers).setIcon(android.R.drawable.ic_menu_save), MenuItem.SHOW_AS_ACTION_IF_ROOM); CompatibilityUtils.setShowAsAction( menu.add(0, MENU_HIERARCHY_VIEW, 0, R.string.view_hierarchy).setIcon(R.drawable.ic_menu_goto), MenuItem.SHOW_AS_ACTION_IF_ROOM); CompatibilityUtils.setShowAsAction(menu.add(0, MENU_LANGUAGES, 0, R.string.change_language) .setIcon(R.drawable.ic_menu_start_conversation), MenuItem.SHOW_AS_ACTION_NEVER); CompatibilityUtils.setShowAsAction(menu.add(0, MENU_PREFERENCES, 0, R.string.general_preferences) .setIcon(R.drawable.ic_menu_preferences), MenuItem.SHOW_AS_ACTION_NEVER); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); FormController formController = Collect.getInstance().getFormController(); boolean useability; useability = mAdminPreferences.getBoolean(AdminPreferencesActivity.KEY_SAVE_MID, true); menu.findItem(MENU_SAVE).setVisible(useability).setEnabled(useability); useability = mAdminPreferences.getBoolean(AdminPreferencesActivity.KEY_JUMP_TO, true); menu.findItem(MENU_HIERARCHY_VIEW).setVisible(useability).setEnabled(useability); useability = mAdminPreferences.getBoolean(AdminPreferencesActivity.KEY_CHANGE_LANGUAGE, true) && (formController != null) && formController.getLanguages() != null && formController.getLanguages().length > 1; menu.findItem(MENU_LANGUAGES).setVisible(useability).setEnabled(useability); useability = mAdminPreferences.getBoolean(AdminPreferencesActivity.KEY_ACCESS_SETTINGS, true); menu.findItem(MENU_PREFERENCES).setVisible(useability).setEnabled(useability); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { FormController formController = Collect.getInstance().getFormController(); switch (item.getItemId()) { case MENU_LANGUAGES: Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected", "MENU_LANGUAGES"); createLanguageDialog(); return true; case MENU_SAVE: Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected", "MENU_SAVE"); // don't exit saveDataToDisk(DO_NOT_EXIT, isInstanceComplete(false), null); return true; case MENU_HIERARCHY_VIEW: Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected", "MENU_HIERARCHY_VIEW"); if (formController.currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } Intent i = new Intent(this, FormHierarchyActivity.class); startActivityForResult(i, HIERARCHY_ACTIVITY); return true; case MENU_PREFERENCES: Collect.getInstance().getActivityLogger().logInstanceAction(this, "onOptionsItemSelected", "MENU_PREFERENCES"); Intent pref = new Intent(this, PreferencesActivity.class); startActivity(pref); return true; } return super.onOptionsItemSelected(item); } /** * Attempt to save the answer(s) in the current screen to into the data * model. * * @param evaluateConstraints * @return false if any error occurs while saving (constraint violated, * etc...), true otherwise. */ private boolean saveAnswersForCurrentScreen(boolean evaluateConstraints) { FormController formController = Collect.getInstance().getFormController(); // only try to save if the current event is a question or a field-list // group if (formController.currentPromptIsQuestion()) { LinkedHashMap<FormIndex, IAnswerData> answers = ((ODKView) mCurrentView).getAnswers(); try { FailedConstraint constraint = formController.saveAllScreenAnswers(answers, evaluateConstraints); if (constraint != null) { createConstraintToast(constraint.index, constraint.status); return false; } } catch (JavaRosaException e) { Log.e(t, e.getMessage(), e); createErrorDialog(e.getCause().getMessage(), DO_NOT_EXIT); return false; } } return true; } /** * Clears the answer on the screen. */ private void clearAnswer(QuestionWidget qw) { if (qw.getAnswer() != null) { qw.clearAnswer(); } } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateContextMenu", "show"); FormController formController = Collect.getInstance().getFormController(); menu.add(0, v.getId(), 0, getString(R.string.clear_answer)); if (formController.indexContainsRepeatableGroup()) { menu.add(0, DELETE_REPEAT, 0, getString(R.string.delete_repeat)); } menu.setHeaderTitle(getString(R.string.edit_prompt)); } @Override public boolean onContextItemSelected(MenuItem item) { /* * We don't have the right view here, so we store the View's ID as the * item ID and loop through the possible views to find the one the user * clicked on. */ for (QuestionWidget qw : ((ODKView) mCurrentView).getWidgets()) { if (item.getItemId() == qw.getId()) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onContextItemSelected", "createClearDialog", qw.getPrompt().getIndex()); createClearDialog(qw); } } if (item.getItemId() == DELETE_REPEAT) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onContextItemSelected", "createDeleteRepeatConfirmDialog"); createDeleteRepeatConfirmDialog(); } return super.onContextItemSelected(item); } /** * If we're loading, then we pass the loading thread to our next instance. */ @Override public Object onRetainNonConfigurationInstance() { FormController formController = Collect.getInstance().getFormController(); // if a form is loading, pass the loader task if (mFormLoaderTask != null && mFormLoaderTask.getStatus() != AsyncTask.Status.FINISHED) return mFormLoaderTask; // if a form is writing to disk, pass the save to disk task if (mSaveToDiskTask != null && mSaveToDiskTask.getStatus() != AsyncTask.Status.FINISHED) return mSaveToDiskTask; // mFormEntryController is static so we don't need to pass it. if (formController != null && formController.currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } return null; } @Override public void onLocationChanged(Location location) { mLocation = location; if (mLocation != null) { // Bug report: cached GeoPoint is being returned as the first value. // Wait for the 2nd value to be returned, which is hopefully not cached? ++mLocationCount; InfoLogger.geolog("GeoPointActivity: " + System.currentTimeMillis() + " onLocationChanged(" + mLocationCount + ") lat: " + mLocation.getLatitude() + " long: " + mLocation.getLongitude() + " acc: " + mLocation.getAccuracy()); Log.i("lat=", Double.toString(mLocation.getLatitude())); Log.i("lat=", Double.toString(mLocation.getLongitude())); Log.i("lat=", Double.toString(mLocation.getAccuracy())); } else { InfoLogger.geolog("GeoPointActivity: " + System.currentTimeMillis() + " onLocationChanged(" + mLocationCount + ") null location"); } } @Override public void onProviderDisabled(String provider) { } @Override public void onProviderEnabled(String provider) { } @Override public void onStatusChanged(String provider, int status, Bundle extras) { switch (status) { case LocationProvider.AVAILABLE: break; case LocationProvider.OUT_OF_SERVICE: break; case LocationProvider.TEMPORARILY_UNAVAILABLE: break; } } /** * Creates a view given the View type and an event * * @param event * @param advancingPage * -- true if this results from advancing through the form * @return newly created View */ private View createView(int event, boolean advancingPage, int swipeCase) { FormController formController = Collect.getInstance().getFormController(); /* setTitle(getString(R.string.app_name) + " > " + formController.getFormTitle());*/ int questioncount = formController.getFormDef().getDeepChildCount(); switch (event) { case FormEntryController.EVENT_BEGINNING_OF_FORM: progressValue = 0.0; progressUpdate(progressValue, questioncount); View startView = View.inflate(this, R.layout.form_entry_start, null); /*setTitle(getString(R.string.app_name) + " > " + formController.getFormTitle());*/ Drawable image = null; File mediaFolder = formController.getMediaFolder(); String mediaDir = mediaFolder.getAbsolutePath(); BitmapDrawable bitImage = null; // attempt to load the form-specific logo... // this is arbitrarily silly bitImage = new BitmapDrawable(getResources(), mediaDir + File.separator + "form_logo.png"); if (bitImage != null && bitImage.getBitmap() != null && bitImage.getIntrinsicHeight() > 0 && bitImage.getIntrinsicWidth() > 0) { image = bitImage; } if (image == null) { // show the opendatakit zig... // image = // getResources().getDrawable(R.drawable.opendatakit_zig); ((ImageView) startView.findViewById(R.id.form_start_bling)).setVisibility(View.GONE); } else { ImageView v = ((ImageView) startView.findViewById(R.id.form_start_bling)); v.setImageDrawable(image); v.setContentDescription(formController.getFormTitle()); } // change start screen based on navigation prefs String navigationChoice = PreferenceManager.getDefaultSharedPreferences(this) .getString(PreferencesActivity.KEY_NAVIGATION, PreferencesActivity.KEY_NAVIGATION); Boolean useSwipe = false; Boolean useButtons = false; ImageView ia = ((ImageView) startView.findViewById(R.id.image_advance)); ImageView ib = ((ImageView) startView.findViewById(R.id.image_backup)); TextView ta = ((TextView) startView.findViewById(R.id.text_advance)); TextView tb = ((TextView) startView.findViewById(R.id.text_backup)); TextView d = ((TextView) startView.findViewById(R.id.description)); if (navigationChoice != null) { if (navigationChoice.contains(PreferencesActivity.NAVIGATION_SWIPE)) { useSwipe = true; } if (navigationChoice.contains(PreferencesActivity.NAVIGATION_BUTTONS)) { useButtons = true; } } if (useSwipe && !useButtons) { d.setText(getString(R.string.swipe_instructions, formController.getFormTitle())); } else if (useButtons && !useSwipe) { ia.setVisibility(View.GONE); ib.setVisibility(View.GONE); ta.setVisibility(View.GONE); tb.setVisibility(View.GONE); d.setText(getString(R.string.buttons_instructions, formController.getFormTitle())); } else { d.setText(getString(R.string.swipe_buttons_instructions, formController.getFormTitle())); } if (mBackButton.isShown()) { mBackButton.setEnabled(false); } if (mNextButton.isShown()) { mNextButton.setEnabled(true); } return startView; case FormEntryController.EVENT_END_OF_FORM: progressValue = questioncount; progressUpdate(progressValue, questioncount); View endView = View.inflate(this, R.layout.form_entry_end, null); ((ImageView) endView.findViewById(R.id.completed)) .setImageDrawable(getResources().getDrawable(R.drawable.complete_tick)); ((TextView) endView.findViewById(R.id.description)) .setText(getString(R.string.save_enter_data_description, formController.getFormTitle())); // checkbox for if finished or ready to send final CheckBox instanceComplete = ((CheckBox) endView.findViewById(R.id.mark_finished)); instanceComplete.setChecked(isInstanceComplete(true)); if (!mAdminPreferences.getBoolean(AdminPreferencesActivity.KEY_MARK_AS_FINALIZED, true)) { instanceComplete.setVisibility(View.GONE); } // edittext to change the displayed name of the instance final EditText saveAs = (EditText) endView.findViewById(R.id.save_name); // disallow carriage returns in the name InputFilter returnFilter = new InputFilter() { public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { for (int i = start; i < end; i++) { if (Character.getType((source.charAt(i))) == Character.CONTROL) { return ""; } } return null; } }; saveAs.setFilters(new InputFilter[] { returnFilter }); String saveName = getSaveName(); if (saveName == null) { // no meta/instanceName field in the form -- see if we have a // name for this instance from a previous save attempt... if (getContentResolver().getType(getIntent().getData()) == InstanceColumns.CONTENT_ITEM_TYPE) { Uri instanceUri = getIntent().getData(); Cursor instance = null; try { instance = getContentResolver().query(instanceUri, null, null, null, null); if (instance.getCount() == 1) { instance.moveToFirst(); saveName = instance.getString(instance.getColumnIndex(InstanceColumns.DISPLAY_NAME)); } } finally { if (instance != null) { instance.close(); } } } if (saveName == null) { // last resort, default to the form title saveName = formController.getFormTitle(); } // present the prompt to allow user to name the form TextView sa = (TextView) endView.findViewById(R.id.save_form_as); sa.setVisibility(View.VISIBLE); saveAs.setText(saveName); saveAs.setEnabled(true); saveAs.setVisibility(View.VISIBLE); } else { // if instanceName is defined in form, this is the name -- no // revisions // display only the name, not the prompt, and disable edits TextView sa = (TextView) endView.findViewById(R.id.save_form_as); sa.setVisibility(View.GONE); saveAs.setText(saveName); saveAs.setEnabled(false); saveAs.setBackgroundColor(Color.WHITE); saveAs.setVisibility(View.VISIBLE); } // override the visibility settings based upon admin preferences if (!mAdminPreferences.getBoolean(AdminPreferencesActivity.KEY_SAVE_AS, true)) { saveAs.setVisibility(View.GONE); TextView sa = (TextView) endView.findViewById(R.id.save_form_as); sa.setVisibility(View.GONE); } // Create 'save' button ((Button) endView.findViewById(R.id.save_exit_button)).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createView.saveAndExit", instanceComplete.isChecked() ? "saveAsComplete" : "saveIncomplete"); // Form is marked as 'saved' here. if (saveAs.getText().length() < 1) { Toast.makeText(FormEntryActivity.this, R.string.save_as_error, Toast.LENGTH_SHORT).show(); } else { saveDataToDisk(EXIT, instanceComplete.isChecked(), saveAs.getText().toString()); } } }); if (mBackButton.isShown()) { mBackButton.setEnabled(true); } if (mNextButton.isShown()) { mNextButton.setEnabled(false); } return endView; case FormEntryController.EVENT_QUESTION: case FormEntryController.EVENT_GROUP: case FormEntryController.EVENT_REPEAT: ODKView odkv = null; // should only be a group here if the event_group is a field-list try { double depth = (double) formController.getFormIndex().getDepth(); double local_index = (double) formController.getFormIndex().getLocalIndex(); double instance_index = (double) formController.getFormIndex().getInstanceIndex(); Log.i("Question coint", Integer.toString(questioncount)); Log.i("Depth", Integer.toString(formController.getFormIndex().getDepth())); Log.i("Local Index", Integer.toString(formController.getFormIndex().getLocalIndex())); Log.i("Instance Index", Integer.toString(formController.getFormIndex().getInstanceIndex())); if (swipeCase == NEXT) { Log.i("progressValue", Double.toString(progressValue)); } if (swipeCase == PREVIOUS) { progressValue = progressValue - (1 - (instance_index * local_index / questioncount * depth)); Log.i("progressValue", Double.toString(progressValue)); } Log.i("progressValue", Double.toString(progressValue)); progressUpdate(progressValue, questioncount); FormEntryPrompt[] prompts = formController.getQuestionPrompts(); FormEntryCaption[] groups = formController.getGroupsForCurrentIndex(); odkv = new ODKView(this, formController.getQuestionPrompts(), groups, advancingPage); Log.i(t, "created view for group " + (groups.length > 0 ? groups[groups.length - 1].getLongText() : "[top]") + " " + (prompts.length > 0 ? prompts[0].getQuestionText() : "[no question]")); } catch (RuntimeException e) { Log.e(t, e.getMessage(), e); // this is badness to avoid a crash. try { event = formController.stepToNextScreenEvent(); createErrorDialog(e.getMessage(), DO_NOT_EXIT); } catch (JavaRosaException e1) { Log.e(t, e1.getMessage(), e1); createErrorDialog(e.getMessage() + "\n\n" + e1.getCause().getMessage(), DO_NOT_EXIT); } return createView(event, advancingPage, swipeCase); } // Makes a "clear answer" menu pop up on long-click for (QuestionWidget qw : odkv.getWidgets()) { if (!qw.getPrompt().isReadOnly()) { registerForContextMenu(qw); } } if (mBackButton.isShown() && mNextButton.isShown()) { mBackButton.setEnabled(true); mNextButton.setEnabled(true); } return odkv; default: Log.e(t, "Attempted to create a view that does not exist."); // this is badness to avoid a crash. try { event = formController.stepToNextScreenEvent(); createErrorDialog(getString(R.string.survey_internal_error), EXIT); } catch (JavaRosaException e) { Log.e(t, e.getMessage(), e); createErrorDialog(e.getCause().getMessage(), EXIT); } return createView(event, advancingPage, swipeCase); } } private void progressUpdate(double progressValue, int questioncount) { double progress = (progressValue / (double) questioncount) * 100; Log.i("Progess", Double.toString(progress)); //progressBar.setProgress((int) progress); Log.i("Index", Integer.toString(questioncount)); } public String getSaveName() { //TODO FormController formController = Collect.getInstance().getFormController(); String saveName = formController.getSubmissionMetadata().instanceName; Log.i("Save name", Boolean.toString(mAdminPreferences.getBoolean("savename_from_input", true))); if (mAdminPreferences.getBoolean("savename_from_input", true)) { Log.i("Save name", mAdminPreferences.getString("defaultsave_field", getString(R.string.default_save_name))); if (!mAdminPreferences.getString("defaultsave_field", getString(R.string.default_save_name)) .equals("")) { try { ByteArrayPayload submissionXml = formController.getSubmissionXml(); InputStream submissionInput = submissionXml.getPayloadStream(); XmlPullParserFactory xmlFactoryObject = XmlPullParserFactory.newInstance(); XmlPullParser myparser = xmlFactoryObject.newPullParser(); myparser.setInput(new StringReader(IOUtils.toString(submissionInput, "UTF-8"))); Log.i("SAVENAME_SUBMISSION", IOUtils.toString(submissionInput, "UTF-8")); saveName = parseSaveName( mAdminPreferences.getString("defaultsave_field", getString(R.string.default_save_name)), myparser, formController.getSubmissionMetadata().instanceName); } catch (IOException e) { e.printStackTrace(); } catch (XmlPullParserException e) { e.printStackTrace(); } } } return saveName; } public String parseSaveName(String valuesToGet, XmlPullParser xmlToParse, String defaultValue) { String saveName = ""; String[] fields = valuesToGet.split("\\s+"); System.out.println(xmlToParse.getAttributeCount()); for (int i = 0; i < fields.length; i++) { String userinput = getValueFromXML(xmlToParse, fields[i]); Log.i("PARSINGKEY", fields[i]); if (userinput != null) { saveName = saveName + userinput + " "; } } if (saveName.equals("")) { saveName = defaultValue; } return saveName; } public String getValueFromXML(XmlPullParser xml, String key) { String result = null; try { int event = xml.getEventType(); String text = null; while (event != XmlPullParser.END_DOCUMENT) { String tag = xml.getName(); if (tag != null) { System.out.println(tag); } else { System.out.println("Null Tag"); } switch (event) { case XmlPullParser.START_TAG: break; case XmlPullParser.TEXT: text = xml.getText(); break; case XmlPullParser.END_TAG: if (tag.equals(key)) { System.out.println("Match Found"); result = text; System.out.println(result); } break; } event = xml.next(); } } catch (Exception e) { e.printStackTrace(); } return result; } @Override public boolean dispatchTouchEvent(MotionEvent mv) { boolean handled = mGestureDetector.onTouchEvent(mv); if (!handled) { return super.dispatchTouchEvent(mv); } return handled; // this is always true } /** * Determines what should be displayed on the screen. Possible options are: * a question, an ask repeat dialog, or the submit screen. Also saves * answers to the data model after checking constraints. */ private void showNextView() { try { FormController formController = Collect.getInstance().getFormController(); // get constraint behavior preference value with appropriate default String constraint_behavior = PreferenceManager.getDefaultSharedPreferences(this).getString( PreferencesActivity.KEY_CONSTRAINT_BEHAVIOR, PreferencesActivity.CONSTRAINT_BEHAVIOR_DEFAULT); if (formController.currentPromptIsQuestion()) { // if constraint behavior says we should validate on swipe, do so if (constraint_behavior.equals(PreferencesActivity.CONSTRAINT_BEHAVIOR_ON_SWIPE)) { if (!saveAnswersForCurrentScreen(EVALUATE_CONSTRAINTS)) { // A constraint was violated so a dialog should be showing. mBeenSwiped = false; return; } // otherwise, just save without validating (constraints will be validated on finalize) } else saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } View next; int event = formController.stepToNextScreenEvent(); switch (event) { case FormEntryController.EVENT_QUESTION: case FormEntryController.EVENT_GROUP: // create a savepoint if ((++viewCount) % SAVEPOINT_INTERVAL == 0) { nonblockingCreateSavePointData(); } next = createView(event, true, NEXT); showView(next, AnimationType.RIGHT); break; case FormEntryController.EVENT_END_OF_FORM: case FormEntryController.EVENT_REPEAT: next = createView(event, true, NEXT); showView(next, AnimationType.RIGHT); break; case FormEntryController.EVENT_PROMPT_NEW_REPEAT: createRepeatDialog(); break; case FormEntryController.EVENT_REPEAT_JUNCTURE: Log.i(t, "repeat juncture: " + formController.getFormIndex().getReference()); // skip repeat junctures until we implement them break; default: Log.w(t, "JavaRosa added a new EVENT type and didn't tell us... shame on them."); break; } } catch (JavaRosaException e) { Log.e(t, e.getMessage(), e); createErrorDialog(e.getCause().getMessage(), DO_NOT_EXIT); } } /** * Determines what should be displayed between a question, or the start * screen and displays the appropriate view. Also saves answers to the data * model without checking constraints. */ private void showPreviousView() { try { FormController formController = Collect.getInstance().getFormController(); // The answer is saved on a back swipe, but question constraints are // ignored. if (formController.currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } if (formController.getEvent() != FormEntryController.EVENT_BEGINNING_OF_FORM) { int event = formController.stepToPreviousScreenEvent(); if (event == FormEntryController.EVENT_BEGINNING_OF_FORM || event == FormEntryController.EVENT_GROUP || event == FormEntryController.EVENT_QUESTION) { // create savepoint if ((++viewCount) % SAVEPOINT_INTERVAL == 0) { nonblockingCreateSavePointData(); } } View next = createView(event, false, PREVIOUS); showView(next, AnimationType.LEFT); } else { mBeenSwiped = false; } } catch (JavaRosaException e) { Log.e(t, e.getMessage(), e); createErrorDialog(e.getCause().getMessage(), DO_NOT_EXIT); } } /** * Displays the View specified by the parameter 'next', animating both the * current view and next appropriately given the AnimationType. Also updates * the progress bar. */ public void showView(View next, AnimationType from) { // disable notifications... if (mInAnimation != null) { mInAnimation.setAnimationListener(null); } if (mOutAnimation != null) { mOutAnimation.setAnimationListener(null); } // logging of the view being shown is already done, as this was handled // by createView() switch (from) { case RIGHT: mInAnimation = AnimationUtils.loadAnimation(this, R.anim.push_left_in); mOutAnimation = AnimationUtils.loadAnimation(this, R.anim.push_left_out); 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; } // complete setup for animations... mInAnimation.setAnimationListener(this); mOutAnimation.setAnimationListener(this); // drop keyboard before transition... if (mCurrentView != null) { InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); inputManager.hideSoftInputFromWindow(mCurrentView.getWindowToken(), 0); } RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); // adjust which view is in the layout container... mStaleView = mCurrentView; mCurrentView = next; mQuestionHolder.addView(mCurrentView, lp); mAnimationCompletionSet = 0; if (mStaleView != null) { // start OutAnimation for transition... mStaleView.startAnimation(mOutAnimation); // and remove the old view (MUST occur after start of animation!!!) mQuestionHolder.removeView(mStaleView); } else { mAnimationCompletionSet = 2; } // start InAnimation for transition... mCurrentView.startAnimation(mInAnimation); String logString = ""; switch (from) { case RIGHT: logString = "next"; break; case LEFT: logString = "previous"; break; case FADE: logString = "refresh"; break; } Collect.getInstance().getActivityLogger().logInstanceAction(this, "showView", logString); } // Hopefully someday we can use managed dialogs when the bugs are fixed /* * Ideally, we'd like to use Android to manage dialogs with onCreateDialog() * and onPrepareDialog(), but dialogs with dynamic content are broken in 1.5 * (cupcake). We do use managed dialogs for our static loading * ProgressDialog. The main issue we noticed and are waiting to see fixed * is: onPrepareDialog() is not called after a screen orientation change. * http://code.google.com/p/android/issues/detail?id=1639 */ // /** * Creates and displays a dialog displaying the violated constraint. */ private void createConstraintToast(FormIndex index, int saveStatus) { FormController formController = Collect.getInstance().getFormController(); String constraintText; switch (saveStatus) { case FormEntryController.ANSWER_CONSTRAINT_VIOLATED: Collect.getInstance().getActivityLogger().logInstanceAction(this, "createConstraintToast.ANSWER_CONSTRAINT_VIOLATED", "show", index); constraintText = formController.getQuestionPromptConstraintText(index); if (constraintText == null) { constraintText = formController.getQuestionPrompt(index) .getSpecialFormQuestionText("constraintMsg"); if (constraintText == null) { constraintText = getString(R.string.invalid_answer_error); } } break; case FormEntryController.ANSWER_REQUIRED_BUT_EMPTY: Collect.getInstance().getActivityLogger().logInstanceAction(this, "createConstraintToast.ANSWER_REQUIRED_BUT_EMPTY", "show", index); constraintText = formController.getQuestionPromptRequiredText(index); if (constraintText == null) { constraintText = formController.getQuestionPrompt(index).getSpecialFormQuestionText("requiredMsg"); if (constraintText == null) { constraintText = getString(R.string.required_answer_error); } } break; default: return; } showCustomToast(constraintText, Toast.LENGTH_SHORT); } /** * Creates a toast with the specified message. * * @param message */ private void showCustomToast(String message, int duration) { LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.toast_view, null); // set the text in the view TextView tv = (TextView) view.findViewById(R.id.message); tv.setText(message); Toast t = new Toast(this); t.setView(view); t.setDuration(duration); t.setGravity(Gravity.CENTER, 0, 0); t.show(); } /** * Creates and displays a dialog asking the user if they'd like to create a * repeat of the current group. */ private void createRepeatDialog() { FormController formController = Collect.getInstance().getFormController(); Collect.getInstance().getActivityLogger().logInstanceAction(this, "createRepeatDialog", "show"); mAlertDialog = new AlertDialog.Builder(this).create(); mAlertDialog.setIcon(android.R.drawable.ic_dialog_info); DialogInterface.OnClickListener repeatListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { FormController formController = Collect.getInstance().getFormController(); switch (i) { case DialogInterface.BUTTON_POSITIVE: // yes, repeat Collect.getInstance().getActivityLogger().logInstanceAction(this, "createRepeatDialog", "addRepeat"); try { formController.newRepeat(); } catch (Exception e) { FormEntryActivity.this.createErrorDialog(e.getMessage(), DO_NOT_EXIT); return; } if (!formController.indexIsInFieldList()) { // we are at a REPEAT event that does not have a // field-list appearance // step to the next visible field... // which could be the start of a new repeat group... showNextView(); } else { // we are at a REPEAT event that has a field-list // appearance // just display this REPEAT event's group. refreshCurrentView(); } break; case DialogInterface.BUTTON_NEGATIVE: // no, no repeat Collect.getInstance().getActivityLogger().logInstanceAction(this, "createRepeatDialog", "showNext"); // // Make sure the error dialog will not disappear. // // When showNextView() popups an error dialog (because of a JavaRosaException) // the issue is that the "add new repeat dialog" is referenced by mAlertDialog // like the error dialog. When the "no repeat" is clicked, the error dialog // is shown. Android by default dismisses the dialogs when a button is clicked, // so instead of closing the first dialog, it closes the second. new Thread() { @Override public void run() { FormEntryActivity.this.runOnUiThread(new Runnable() { @Override public void run() { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } showNextView(); } }); } }.start(); break; } } }; if (formController.getLastRepeatCount() > 0) { mAlertDialog.setTitle(getString(R.string.leaving_repeat_ask)); mAlertDialog.setMessage(getString(R.string.add_another_repeat, formController.getLastGroupText())); mAlertDialog.setButton(getString(R.string.add_another), repeatListener); mAlertDialog.setButton2(getString(R.string.leave_repeat_yes), repeatListener); } else { mAlertDialog.setTitle(getString(R.string.entering_repeat_ask)); mAlertDialog.setMessage(getString(R.string.add_repeat, formController.getLastGroupText())); mAlertDialog.setButton(getString(R.string.entering_repeat), repeatListener); mAlertDialog.setButton2(getString(R.string.add_repeat_no), repeatListener); } mAlertDialog.setCancelable(false); mBeenSwiped = false; mAlertDialog.show(); } /** * Creates and displays dialog with the given errorMsg. */ private void createErrorDialog(String errorMsg, final boolean shouldExit) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createErrorDialog", "show." + Boolean.toString(shouldExit)); if (mAlertDialog != null && mAlertDialog.isShowing()) { errorMsg = mErrorMessage + "\n\n" + errorMsg; mErrorMessage = errorMsg; } else { mAlertDialog = new AlertDialog.Builder(this).create(); mErrorMessage = errorMsg; } mAlertDialog.setIcon(android.R.drawable.ic_dialog_info); mAlertDialog.setTitle(getString(R.string.error_occured)); mAlertDialog.setMessage(errorMsg); DialogInterface.OnClickListener errorListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { switch (i) { case DialogInterface.BUTTON_POSITIVE: Collect.getInstance().getActivityLogger().logInstanceAction(this, "createErrorDialog", "OK"); if (shouldExit) { mErrorMessage = null; finish(); } break; } } }; mAlertDialog.setCancelable(false); mAlertDialog.setButton(getString(R.string.ok), errorListener); mBeenSwiped = false; mAlertDialog.show(); } /** * Creates a confirm/cancel dialog for deleting repeats. */ private void createDeleteRepeatConfirmDialog() { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createDeleteRepeatConfirmDialog", "show"); FormController formController = Collect.getInstance().getFormController(); mAlertDialog = new AlertDialog.Builder(this).create(); mAlertDialog.setIcon(android.R.drawable.ic_dialog_info); String name = formController.getLastRepeatedGroupName(); int repeatcount = formController.getLastRepeatedGroupRepeatCount(); if (repeatcount != -1) { name += " (" + (repeatcount + 1) + ")"; } mAlertDialog.setTitle(getString(R.string.delete_repeat_ask)); mAlertDialog.setMessage(getString(R.string.delete_repeat_confirm, name)); DialogInterface.OnClickListener quitListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { FormController formController = Collect.getInstance().getFormController(); switch (i) { case DialogInterface.BUTTON_POSITIVE: // yes Collect.getInstance().getActivityLogger().logInstanceAction(this, "createDeleteRepeatConfirmDialog", "OK"); formController.deleteRepeat(); showPreviousView(); break; case DialogInterface.BUTTON_NEGATIVE: // no Collect.getInstance().getActivityLogger().logInstanceAction(this, "createDeleteRepeatConfirmDialog", "cancel"); break; } } }; mAlertDialog.setCancelable(false); mAlertDialog.setButton(getString(R.string.discard_group), quitListener); mAlertDialog.setButton2(getString(R.string.delete_repeat_no), quitListener); mAlertDialog.show(); } /** * Saves data and writes it to disk. If exit is set, program will exit after * save completes. Complete indicates whether the user has marked the * isntancs as complete. If updatedSaveName is non-null, the instances * content provider is updated with the new name */ private boolean saveDataToDisk(boolean exit, boolean complete, String updatedSaveName) { // save current answer if (!saveAnswersForCurrentScreen(complete)) { Toast.makeText(this, getString(R.string.data_saved_error), Toast.LENGTH_SHORT).show(); return false; } synchronized (saveDialogLock) { mSaveToDiskTask = new SaveToDiskTask(getIntent().getData(), exit, complete, updatedSaveName); mSaveToDiskTask.setFormSavedListener(this); showDialog(SAVING_DIALOG); // show dialog before we execute... mSaveToDiskTask.execute(); } return true; } /** * Create a dialog with options to save and exit, save, or quit without * saving */ private void createQuitDialog() { FormController formController = Collect.getInstance().getFormController(); String[] items; if (mAdminPreferences.getBoolean(AdminPreferencesActivity.KEY_SAVE_MID, true)) { String[] two = { getString(R.string.keep_changes), getString(R.string.do_not_save) }; items = two; } else { String[] one = { getString(R.string.do_not_save) }; items = one; } Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog", "show"); mAlertDialog = new AlertDialog.Builder(this).setIcon(android.R.drawable.ic_dialog_info) .setTitle(getString(R.string.quit_application, formController.getFormTitle())) .setNeutralButton(getString(R.string.do_not_exit), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog", "cancel"); dialog.cancel(); } }).setItems(items, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { switch (which) { case 0: // save and exit // this is slightly complicated because if the // option is disabled in // the admin menu, then case 0 actually becomes // 'discard and exit' // whereas if it's enabled it's 'save and exit' if (mAdminPreferences.getBoolean(AdminPreferencesActivity.KEY_SAVE_MID, true)) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog", "saveAndExit"); saveDataToDisk(EXIT, isInstanceComplete(false), null); } else { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog", "discardAndExit"); removeTempInstance(); finishReturnInstance(); } break; case 1: // discard changes and exit Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog", "discardAndExit"); // close all open databases of external data. Collect.getInstance().getExternalDataManager().close(); removeTempInstance(); finishReturnInstance(); break; case 2:// do nothing Collect.getInstance().getActivityLogger().logInstanceAction(this, "createQuitDialog", "cancel"); break; } } }).create(); mAlertDialog.show(); } /** * this method cleans up unneeded files when the user selects 'discard and * exit' */ private void removeTempInstance() { FormController formController = Collect.getInstance().getFormController(); // attempt to remove any scratch file File temp = SaveToDiskTask.savepointFile(formController.getInstancePath()); if (temp.exists()) { temp.delete(); } String selection = InstanceColumns.INSTANCE_FILE_PATH + "=?"; String[] selectionArgs = { formController.getInstancePath().getAbsolutePath() }; boolean erase = false; { Cursor c = null; try { c = getContentResolver().query(InstanceColumns.CONTENT_URI, null, selection, selectionArgs, null); erase = (c.getCount() < 1); } finally { if (c != null) { c.close(); } } } // if it's not already saved, erase everything if (erase) { // delete media first String instanceFolder = formController.getInstancePath().getParent(); Log.i(t, "attempting to delete: " + instanceFolder); int images = MediaUtils .deleteImagesInFolderFromMediaProvider(formController.getInstancePath().getParentFile()); int audio = MediaUtils .deleteAudioInFolderFromMediaProvider(formController.getInstancePath().getParentFile()); int video = MediaUtils .deleteVideoInFolderFromMediaProvider(formController.getInstancePath().getParentFile()); Log.i(t, "removed from content providers: " + images + " image files, " + audio + " audio files," + " and " + video + " video files."); File f = new File(instanceFolder); if (f.exists() && f.isDirectory()) { for (File del : f.listFiles()) { Log.i(t, "deleting file: " + del.getAbsolutePath()); del.delete(); } f.delete(); } } } /** * Confirm clear answer dialog */ private void createClearDialog(final QuestionWidget qw) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createClearDialog", "show", qw.getPrompt().getIndex()); mAlertDialog = new AlertDialog.Builder(this).create(); mAlertDialog.setIcon(android.R.drawable.ic_dialog_info); mAlertDialog.setTitle(getString(R.string.clear_answer_ask)); String question = qw.getPrompt().getLongText(); if (question == null) { question = ""; } if (question.length() > 50) { question = question.substring(0, 50) + "..."; } mAlertDialog.setMessage(getString(R.string.clearanswer_confirm, question)); DialogInterface.OnClickListener quitListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { switch (i) { case DialogInterface.BUTTON_POSITIVE: // yes Collect.getInstance().getActivityLogger().logInstanceAction(this, "createClearDialog", "clearAnswer", qw.getPrompt().getIndex()); clearAnswer(qw); saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); break; case DialogInterface.BUTTON_NEGATIVE: // no Collect.getInstance().getActivityLogger().logInstanceAction(this, "createClearDialog", "cancel", qw.getPrompt().getIndex()); break; } } }; mAlertDialog.setCancelable(false); mAlertDialog.setButton(getString(R.string.discard_answer), quitListener); mAlertDialog.setButton2(getString(R.string.clear_answer_no), quitListener); mAlertDialog.show(); } /** * Creates and displays a dialog allowing the user to set the language for * the form. */ private void createLanguageDialog() { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createLanguageDialog", "show"); FormController formController = Collect.getInstance().getFormController(); final String[] languages = formController.getLanguages(); int selected = -1; if (languages != null) { String language = formController.getLanguage(); for (int i = 0; i < languages.length; i++) { if (language.equals(languages[i])) { selected = i; } } } mAlertDialog = new AlertDialog.Builder(this) .setSingleChoiceItems(languages, selected, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int whichButton) { FormController formController = Collect.getInstance().getFormController(); // Update the language in the content provider // when selecting a new // language ContentValues values = new ContentValues(); values.put(FormsColumns.LANGUAGE, languages[whichButton]); String selection = FormsColumns.FORM_FILE_PATH + "=?"; String selectArgs[] = { mFormPath }; int updated = getContentResolver().update(FormsColumns.CONTENT_URI, values, selection, selectArgs); Log.i(t, "Updated language to: " + languages[whichButton] + " in " + updated + " rows"); Collect.getInstance().getActivityLogger().logInstanceAction(this, "createLanguageDialog", "changeLanguage." + languages[whichButton]); formController.setLanguage(languages[whichButton]); dialog.dismiss(); if (formController.currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } refreshCurrentView(); } }).setTitle(getString(R.string.change_language)) .setNegativeButton(getString(R.string.do_not_change), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int whichButton) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "createLanguageDialog", "cancel"); } }).create(); mAlertDialog.show(); } /** * We use Android's dialog management for loading/saving progress dialogs */ @Override protected Dialog onCreateDialog(int id) { switch (id) { case PROGRESS_DIALOG: Log.e(t, "Creating PROGRESS_DIALOG"); Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateDialog.PROGRESS_DIALOG", "show"); mProgressDialog = new ProgressDialog(this); DialogInterface.OnClickListener loadingButtonListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateDialog.PROGRESS_DIALOG", "cancel"); dialog.dismiss(); mFormLoaderTask.setFormLoaderListener(null); FormLoaderTask t = mFormLoaderTask; mFormLoaderTask = null; t.cancel(true); t.destroy(); finish(); } }; mProgressDialog.setIcon(android.R.drawable.ic_dialog_info); mProgressDialog.setTitle(getString(R.string.loading_form)); mProgressDialog.setMessage(getString(R.string.please_wait)); mProgressDialog.setIndeterminate(true); mProgressDialog.setCancelable(false); mProgressDialog.setButton(getString(R.string.cancel_loading_form), loadingButtonListener); return mProgressDialog; case SAVING_DIALOG: Log.e(t, "Creating SAVING_DIALOG"); Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateDialog.SAVING_DIALOG", "show"); mProgressDialog = new ProgressDialog(this); DialogInterface.OnClickListener cancelSavingButtonListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateDialog.SAVING_DIALOG", "cancel"); dialog.dismiss(); cancelSaveToDiskTask(); } }; mProgressDialog.setIcon(android.R.drawable.ic_dialog_info); mProgressDialog.setTitle(getString(R.string.saving_form)); mProgressDialog.setMessage(getString(R.string.please_wait)); mProgressDialog.setIndeterminate(true); mProgressDialog.setCancelable(false); mProgressDialog.setButton(getString(R.string.cancel), cancelSavingButtonListener); mProgressDialog.setButton(getString(R.string.cancel_saving_form), cancelSavingButtonListener); mProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateDialog.SAVING_DIALOG", "OnCancelListener"); cancelSaveToDiskTask(); } }); mProgressDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onCreateDialog.SAVING_DIALOG", "OnDismissListener"); cancelSaveToDiskTask(); } }); return mProgressDialog; } return null; } private void cancelSaveToDiskTask() { synchronized (saveDialogLock) { mSaveToDiskTask.setFormSavedListener(null); boolean cancelled = mSaveToDiskTask.cancel(true); Log.w(t, "Cancelled SaveToDiskTask! (" + cancelled + ")"); mSaveToDiskTask = null; } } /** * Dismiss any showing dialogs that we manually manage. */ private void dismissDialogs() { Log.e(t, "Dismiss dialogs"); if (mAlertDialog != null && mAlertDialog.isShowing()) { mAlertDialog.dismiss(); } } @Override protected void onPause() { FormController formController = Collect.getInstance().getFormController(); dismissDialogs(); // make sure we're not already saving to disk. if we are, currentPrompt // is getting constantly updated if (mSaveToDiskTask == null || mSaveToDiskTask.getStatus() == AsyncTask.Status.FINISHED) { if (mCurrentView != null && formController != null && formController.currentPromptIsQuestion()) { saveAnswersForCurrentScreen(DO_NOT_EVALUATE_CONSTRAINTS); } } super.onPause(); } @Override protected void onResume() { super.onResume(); Log.i("Activity", "Resume"); if (needLocation) { Log.i("It is starting", "gps"); if (mLocationManager != null) { mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 5000, 5, this); } if (mErrorMessage != null) { if (mAlertDialog != null && !mAlertDialog.isShowing()) { createErrorDialog(mErrorMessage, EXIT); } else { return; } } } FormController formController = Collect.getInstance().getFormController(); Collect.getInstance().getActivityLogger().open(); if (mFormLoaderTask != null) { mFormLoaderTask.setFormLoaderListener(this); if (formController == null && mFormLoaderTask.getStatus() == AsyncTask.Status.FINISHED) { FormController fec = mFormLoaderTask.getFormController(); if (fec != null) { loadingComplete(mFormLoaderTask); } else { dismissDialog(PROGRESS_DIALOG); FormLoaderTask t = mFormLoaderTask; mFormLoaderTask = null; t.cancel(true); t.destroy(); // there is no formController -- fire MainMenu activity? startActivity(new Intent(this, MainMenuActivity.class)); } } } else { if (formController == null) { // there is no formController -- fire MainMenu activity? startActivity(new Intent(this, MainMenuActivity.class)); return; } else { refreshCurrentView(); } } if (mSaveToDiskTask != null) { mSaveToDiskTask.setFormSavedListener(this); } // only check the buttons if it's enabled in preferences SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); String navigation = sharedPreferences.getString(PreferencesActivity.KEY_NAVIGATION, PreferencesActivity.KEY_NAVIGATION); Boolean showButtons = false; if (navigation.contains(PreferencesActivity.NAVIGATION_BUTTONS)) { showButtons = true; } if (showButtons) { mBackButton.setVisibility(View.VISIBLE); mNextButton.setVisibility(View.VISIBLE); } else { mBackButton.setVisibility(View.GONE); mNextButton.setVisibility(View.GONE); } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_BACK: Collect.getInstance().getActivityLogger().logInstanceAction(this, "onKeyDown.KEYCODE_BACK", "quit"); createQuitDialog(); return true; case KeyEvent.KEYCODE_DPAD_RIGHT: if (event.isAltPressed() && !mBeenSwiped) { mBeenSwiped = true; Collect.getInstance().getActivityLogger().logInstanceAction(this, "onKeyDown.KEYCODE_DPAD_RIGHT", "showNext"); showNextView(); return true; } break; case KeyEvent.KEYCODE_DPAD_LEFT: if (event.isAltPressed() && !mBeenSwiped) { mBeenSwiped = true; Collect.getInstance().getActivityLogger().logInstanceAction(this, "onKeyDown.KEYCODE_DPAD_LEFT", "showPrevious"); showPreviousView(); return true; } break; } return super.onKeyDown(keyCode, event); } @Override protected void onDestroy() { Log.i("Activity", "Destroyed"); if (mLocationManager != null) { mLocationManager.removeUpdates(this); } if (mFormLoaderTask != null) { mFormLoaderTask.setFormLoaderListener(null); // We have to call cancel to terminate the thread, otherwise it // lives on and retains the FEC in memory. // but only if it's done, otherwise the thread never returns if (mFormLoaderTask.getStatus() == AsyncTask.Status.FINISHED) { FormLoaderTask t = mFormLoaderTask; mFormLoaderTask = null; t.cancel(true); t.destroy(); } } if (mSaveToDiskTask != null) { mSaveToDiskTask.setFormSavedListener(null); // We have to call cancel to terminate the thread, otherwise it // lives on and retains the FEC in memory. if (mSaveToDiskTask.getStatus() == AsyncTask.Status.FINISHED) { mSaveToDiskTask.cancel(true); mSaveToDiskTask = null; } } super.onDestroy(); } private int mAnimationCompletionSet = 0; private void afterAllAnimations() { Log.i(t, "afterAllAnimations"); if (mStaleView != null) { if (mStaleView instanceof ODKView) { // http://code.google.com/p/android/issues/detail?id=8488 ((ODKView) mStaleView).recycleDrawables(); } mStaleView = null; } if (mCurrentView instanceof ODKView) { ((ODKView) mCurrentView).setFocus(this); } mBeenSwiped = false; } @Override public void onAnimationEnd(Animation animation) { Log.i(t, "onAnimationEnd " + ((animation == mInAnimation) ? "in" : ((animation == mOutAnimation) ? "out" : "other"))); if (mInAnimation == animation) { mAnimationCompletionSet |= 1; } else if (mOutAnimation == animation) { mAnimationCompletionSet |= 2; } else { Log.e(t, "Unexpected animation"); } if (mAnimationCompletionSet == 3) { this.afterAllAnimations(); } } @Override public void onAnimationRepeat(Animation animation) { // Added by AnimationListener interface. Log.i(t, "onAnimationRepeat " + ((animation == mInAnimation) ? "in" : ((animation == mOutAnimation) ? "out" : "other"))); } @Override public void onAnimationStart(Animation animation) { // Added by AnimationListener interface. Log.i(t, "onAnimationStart " + ((animation == mInAnimation) ? "in" : ((animation == mOutAnimation) ? "out" : "other"))); } /** * loadingComplete() is called by FormLoaderTask once it has finished * loading a form. */ @Override public void loadingComplete(FormLoaderTask task) { dismissDialog(PROGRESS_DIALOG); Log.i("Form Loading", "Complete"); FormController formController = task.getFormController(); boolean pendingActivityResult = task.hasPendingActivityResult(); boolean hasUsedSavepoint = task.hasUsedSavepoint(); int requestCode = task.getRequestCode(); // these are bogus if Log.i("Reqest Code", Integer.toString(requestCode));// pendingActivityResult is // false int resultCode = task.getResultCode(); Intent intent = task.getIntent(); mFormLoaderTask.setFormLoaderListener(null); FormLoaderTask t = mFormLoaderTask; mFormLoaderTask = null; t.cancel(true); t.destroy(); Collect.getInstance().setFormController(formController); CompatibilityUtils.invalidateOptionsMenu(this); Collect.getInstance().setExternalDataManager(task.getExternalDataManager()); // Set the language if one has already been set in the past String[] languageTest = formController.getLanguages(); if (languageTest != null) { String defaultLanguage = formController.getLanguage(); String newLanguage = ""; String selection = FormsColumns.FORM_FILE_PATH + "=?"; String selectArgs[] = { mFormPath }; Cursor c = null; try { c = getContentResolver().query(FormsColumns.CONTENT_URI, null, selection, selectArgs, null); if (c.getCount() == 1) { c.moveToFirst(); newLanguage = c.getString(c.getColumnIndex(FormsColumns.LANGUAGE)); } } finally { if (c != null) { c.close(); } } // if somehow we end up with a bad language, set it to the default try { formController.setLanguage(newLanguage); } catch (Exception e) { formController.setLanguage(defaultLanguage); } } if (pendingActivityResult) { // set the current view to whatever group we were at... refreshCurrentView(); // process the pending activity request... onActivityResult(requestCode, resultCode, intent); return; } // it can be a normal flow for a pending activity result to restore from // a savepoint // (the call flow handled by the above if statement). For all other use // cases, the // user should be notified, as it means they wandered off doing other // things then // returned to ODK Collect and chose Edit Saved Form, but that the // savepoint for that // form is newer than the last saved version of their form data. if (hasUsedSavepoint) { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(FormEntryActivity.this, getString(R.string.savepoint_used), Toast.LENGTH_LONG) .show(); } }); } // Set saved answer path if (formController.getInstancePath() == null) { // Create new answer folder. String time = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.ENGLISH) .format(Calendar.getInstance().getTime()); String file = mFormPath.substring(mFormPath.lastIndexOf('/') + 1, mFormPath.lastIndexOf('.')); String path = Collect.INSTANCES_PATH + File.separator + file + "_" + time; if (FileUtils.createFolder(path)) { formController.setInstancePath(new File(path + File.separator + file + "_" + time + ".xml")); } } else { Intent reqIntent = getIntent(); boolean showFirst = reqIntent.getBooleanExtra("start", false); if (!showFirst) { // we've just loaded a saved form, so start in the hierarchy // view Intent i = new Intent(this, FormHierarchyActivity.class); startActivity(i); return; // so we don't show the intro screen before jumping to // the hierarchy } } refreshCurrentView(); } /** * called by the FormLoaderTask if something goes wrong. */ @Override public void loadingError(String errorMsg) { dismissDialog(PROGRESS_DIALOG); if (errorMsg != null) { createErrorDialog(errorMsg, EXIT); } else { createErrorDialog(getString(R.string.parse_error), EXIT); } } /** * Called by SavetoDiskTask if everything saves correctly. */ @Override public void savingComplete(SaveResult saveResult) { dismissDialog(SAVING_DIALOG); int saveStatus = saveResult.getSaveResult(); switch (saveStatus) { case SaveToDiskTask.SAVED: Toast.makeText(this, getString(R.string.data_saved_ok), Toast.LENGTH_SHORT).show(); sendSavedBroadcast(); break; case SaveToDiskTask.SAVED_AND_EXIT: Toast.makeText(this, getString(R.string.data_saved_ok), Toast.LENGTH_SHORT).show(); sendSavedBroadcast(); finishReturnInstance(); break; case SaveToDiskTask.SAVE_ERROR: String message; if (saveResult.getSaveErrorMessage() != null) { message = getString(R.string.data_saved_error) + ": " + saveResult.getSaveErrorMessage(); } else { message = getString(R.string.data_saved_error); } Toast.makeText(this, message, Toast.LENGTH_LONG).show(); break; case FormEntryController.ANSWER_CONSTRAINT_VIOLATED: case FormEntryController.ANSWER_REQUIRED_BUT_EMPTY: refreshCurrentView(); // get constraint behavior preference value with appropriate default String constraint_behavior = PreferenceManager.getDefaultSharedPreferences(this).getString( PreferencesActivity.KEY_CONSTRAINT_BEHAVIOR, PreferencesActivity.CONSTRAINT_BEHAVIOR_DEFAULT); // an answer constraint was violated, so we need to display the proper toast(s) // if constraint behavior is on_swipe, this will happen if we do a 'swipe' to the next question if (constraint_behavior.equals(PreferencesActivity.CONSTRAINT_BEHAVIOR_ON_SWIPE)) next(); // otherwise, we can get the proper toast(s) by saving with constraint check else saveAnswersForCurrentScreen(EVALUATE_CONSTRAINTS); break; } } @Override public void onProgressStep(String stepMessage) { this.stepMessage = stepMessage; if (mProgressDialog != null) { mProgressDialog.setMessage(getString(R.string.please_wait) + "\n\n" + stepMessage); } } /** * Attempts to save an answer to the specified index. * * @param answer * @param index * @param evaluateConstraints * @return status as determined in FormEntryController */ public int saveAnswer(IAnswerData answer, FormIndex index, boolean evaluateConstraints) throws JavaRosaException { FormController formController = Collect.getInstance().getFormController(); if (evaluateConstraints) { return formController.answerQuestion(index, answer); } else { formController.saveAnswer(index, answer); return FormEntryController.ANSWER_OK; } } /** * Checks the database to determine if the current instance being edited has * already been 'marked completed'. A form can be 'unmarked' complete and * then resaved. * * @return true if form has been marked completed, false otherwise. */ private boolean isInstanceComplete(boolean end) { FormController formController = Collect.getInstance().getFormController(); // default to false if we're mid form boolean complete = false; // if we're at the end of the form, then check the preferences if (end) { // First get the value from the preferences SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); complete = sharedPreferences.getBoolean(PreferencesActivity.KEY_COMPLETED_DEFAULT, true); } // Then see if we've already marked this form as complete before String selection = InstanceColumns.INSTANCE_FILE_PATH + "=?"; String[] selectionArgs = { formController.getInstancePath().getAbsolutePath() }; Cursor c = null; try { c = getContentResolver().query(InstanceColumns.CONTENT_URI, null, selection, selectionArgs, null); if (c != null && c.getCount() > 0) { c.moveToFirst(); String status = c.getString(c.getColumnIndex(InstanceColumns.STATUS)); if (InstanceProviderAPI.STATUS_COMPLETE.compareTo(status) == 0) { complete = true; } } } finally { if (c != null) { c.close(); } } return complete; } public void next() { if (!mBeenSwiped) { mBeenSwiped = true; showNextView(); } } /** * Returns the instance that was just filled out to the calling activity, if * requested. */ private void finishReturnInstance() { FormController formController = Collect.getInstance().getFormController(); String action = getIntent().getAction(); if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_EDIT.equals(action)) { // caller is waiting on a picked form String selection = InstanceColumns.INSTANCE_FILE_PATH + "=?"; String[] selectionArgs = { formController.getInstancePath().getAbsolutePath() }; Cursor c = null; try { c = getContentResolver().query(InstanceColumns.CONTENT_URI, null, selection, selectionArgs, null); if (c.getCount() > 0) { // should only be one... c.moveToFirst(); String id = c.getString(c.getColumnIndex(InstanceColumns._ID)); Uri instance = Uri.withAppendedPath(InstanceColumns.CONTENT_URI, id); setResult(RESULT_OK, new Intent().setData(instance)); } } finally { if (c != null) { c.close(); } } } finish(); } @Override public boolean onDown(MotionEvent e) { return false; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // only check the swipe if it's enabled in preferences SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); String navigation = sharedPreferences.getString(PreferencesActivity.KEY_NAVIGATION, PreferencesActivity.NAVIGATION_SWIPE); Boolean doSwipe = false; if (navigation.contains(PreferencesActivity.NAVIGATION_SWIPE)) { doSwipe = true; } if (doSwipe) { // Looks for user swipes. If the user has swiped, move to the // appropriate screen. // for all screens a swipe is left/right of at least // .25" and up/down of less than .25" // OR left/right of > .5" DisplayMetrics dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dm); int xPixelLimit = (int) (dm.xdpi * .25); int yPixelLimit = (int) (dm.ydpi * .25); if (mCurrentView instanceof ODKView) { if (((ODKView) mCurrentView).suppressFlingGesture(e1, e2, velocityX, velocityY)) { return false; } } if (mBeenSwiped) { return false; } if ((Math.abs(e1.getX() - e2.getX()) > xPixelLimit && Math.abs(e1.getY() - e2.getY()) < yPixelLimit) || Math.abs(e1.getX() - e2.getX()) > xPixelLimit * 2) { mBeenSwiped = true; if (velocityX > 0) { if (e1.getX() > e2.getX()) { Log.e(t, "showNextView VelocityX is bogus! " + e1.getX() + " > " + e2.getX()); Collect.getInstance().getActivityLogger().logInstanceAction(this, "onFling", "showNext"); showNextView(); } else { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onFling", "showPrevious"); showPreviousView(); } } else { if (e1.getX() < e2.getX()) { Log.e(t, "showPreviousView VelocityX is bogus! " + e1.getX() + " < " + e2.getX()); Collect.getInstance().getActivityLogger().logInstanceAction(this, "onFling", "showPrevious"); showPreviousView(); } else { Collect.getInstance().getActivityLogger().logInstanceAction(this, "onFling", "showNext"); showNextView(); } } return true; } } return false; } @Override public void onLongPress(MotionEvent e) { } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // The onFling() captures the 'up' event so our view thinks it gets long // pressed. // We don't wnat that, so cancel it. if (mCurrentView != null) { mCurrentView.cancelLongPress(); } return false; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public void advance() { next(); } @Override protected void onStart() { super.onStart(); Log.i("Activity", "Start"); if (mLocationManager != null) { mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 5000, 5, this); } Collect.getInstance().getActivityLogger().logOnStart(this); } @Override protected void onStop() { Log.i("Activity", "Stop"); Collect.getInstance().getActivityLogger().logOnStop(this); if (mLocationManager != null) { mLocationManager.removeUpdates(this); } super.onStop(); } private void sendSavedBroadcast() { Intent i = new Intent(); i.setAction("com.geoodk.collect.android.FormSaved"); this.sendBroadcast(i); } @Override public void onSavePointError(String errorMessage) { if (errorMessage != null && errorMessage.trim().length() > 0) { Toast.makeText(this, getString(R.string.save_point_error, errorMessage), Toast.LENGTH_LONG).show(); } } }