package org.commcare.activities;
import android.annotation.SuppressLint;
import android.app.ActionBar;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.graphics.Rect;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore.Images;
import android.util.Log;
import android.util.Pair;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;
import android.widget.VideoView;
import org.commcare.CommCareApplication;
import org.commcare.activities.components.FormEntryConstants;
import org.commcare.activities.components.FormEntryDialogs;
import org.commcare.activities.components.FormEntryInstanceState;
import org.commcare.activities.components.FormEntrySessionWrapper;
import org.commcare.activities.components.FormFileSystemHelpers;
import org.commcare.activities.components.FormNavigationUI;
import org.commcare.activities.components.ImageCaptureProcessing;
import org.commcare.android.javarosa.PollSensorController;
import org.commcare.dalvik.BuildConfig;
import org.commcare.dalvik.R;
import org.commcare.interfaces.CommCareActivityUIController;
import org.commcare.interfaces.WithUIController;
import org.commcare.logic.AndroidFormController;
import org.commcare.utils.CompoundIntentList;
import org.commcare.views.media.MediaLayout;
import org.commcare.android.javarosa.IntentCallout;
import org.commcare.android.javarosa.PollSensorAction;
import org.commcare.interfaces.AdvanceToNextListener;
import org.commcare.interfaces.FormSaveCallback;
import org.commcare.interfaces.FormSavedListener;
import org.commcare.interfaces.WidgetChangedListener;
import org.commcare.logging.AndroidLogger;
import org.commcare.google.services.analytics.GoogleAnalyticsFields;
import org.commcare.google.services.analytics.GoogleAnalyticsUtils;
import org.commcare.logging.analytics.TimedStatsTracker;
import org.commcare.logic.AndroidPropertyManager;
import org.commcare.models.ODKStorage;
import org.commcare.preferences.FormEntryPreferences;
import org.commcare.provider.FormsProviderAPI.FormsColumns;
import org.commcare.provider.InstanceProviderAPI.InstanceColumns;
import org.commcare.tasks.FormLoaderTask;
import org.commcare.tasks.SaveToDiskTask;
import org.commcare.utils.Base64Wrapper;
import org.commcare.utils.FormUploadUtil;
import org.commcare.utils.GeoUtils;
import org.commcare.utils.SessionUnavailableException;
import org.commcare.utils.StringUtils;
import org.commcare.utils.UriToFilePath;
import org.commcare.views.QuestionsView;
import org.commcare.views.ResizingImageView;
import org.commcare.views.UserfacingErrorHandling;
import org.commcare.views.dialogs.CustomProgressDialog;
import org.commcare.views.widgets.BarcodeWidget;
import org.commcare.views.widgets.IntentWidget;
import org.commcare.views.widgets.QuestionWidget;
import org.javarosa.core.model.FormIndex;
import org.javarosa.core.model.data.IAnswerData;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.PropertyManager;
import org.javarosa.core.services.locale.Localization;
import org.javarosa.form.api.FormEntryController;
import org.javarosa.xpath.XPathException;
import org.javarosa.xpath.XPathTypeMismatchException;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.crypto.spec.SecretKeySpec;
/**
* Displays questions, animates transitions between
* questions, and allows the user to enter data.
*/
public class FormEntryActivity extends SaveSessionCommCareActivity<FormEntryActivity>
implements FormSavedListener, FormSaveCallback,
WithUIController, AdvanceToNextListener, WidgetChangedListener {
private static final String TAG = FormEntryActivity.class.getSimpleName();
public static final String KEY_FORM_CONTENT_URI = "form_content_uri";
public static final String KEY_INSTANCE_CONTENT_URI = "instance_content_uri";
public static final String KEY_AES_STORAGE_KEY = "key_aes_storage";
public static final String KEY_HEADER_STRING = "form_header";
public static final String KEY_INCOMPLETE_ENABLED = "org.odk.collect.form.management";
public static final String KEY_RESIZING_ENABLED = "org.odk.collect.resizing.enabled";
private static final String KEY_HAS_SAVED = "org.odk.collect.form.has.saved";
private static final String KEY_WIDGET_WITH_VIDEO_PLAYING = "index-of-widget-with-video-playing-on-pause";
private static final String KEY_POSITION_OF_VIDEO_PLAYING = "position-of-video-playing-on-pause";
// Identifies whether this is a new form, or reloading a form after a screen
// rotation (or similar)
private static final String KEY_FORM_LOAD_HAS_TRIGGERED = "newform";
private static final String KEY_FORM_LOAD_FAILED = "form-failed";
private static final String KEY_LOC_ERROR = "location-not-enabled";
private static final String KEY_LOC_ERROR_PATH = "location-based-xpath-error";
private FormEntryInstanceState instanceState;
private FormEntrySessionWrapper formEntryRestoreSession = new FormEntrySessionWrapper();
private SecretKeySpec symetricKey = null;
public static AndroidFormController mFormController;
private boolean mIncompleteEnabled = true;
private boolean hasFormLoadBeenTriggered = false;
private boolean hasFormLoadFailed = false;
private String locationRecieverErrorAction = null;
private String badLocationXpath = null;
private GestureDetector mGestureDetector;
private int indexOfWidgetWithVideoPlaying = -1;
private int positionOfVideoProgress = -1;
private FormLoaderTask<FormEntryActivity> mFormLoaderTask;
private SaveToDiskTask mSaveToDiskTask;
private Uri formProviderContentURI = FormsColumns.CONTENT_URI;
private Uri instanceProviderContentURI = InstanceColumns.CONTENT_URI;
private static String mHeaderString;
// Was the form saved? Used to set activity return code.
private boolean hasSaved = false;
private BroadcastReceiver mLocationServiceIssueReceiver;
// marked true if we are in the process of saving a form because the user
// database & key session are expiring. Being set causes savingComplete to
// broadcast a form saving intent.
private boolean savingFormOnKeySessionExpiration = false;
private FormEntryActivityUIController uiController;
@Override
@SuppressLint("NewApi")
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
instanceState = new FormEntryInstanceState();
// must be at the beginning of any activity that can be called from an external intent
try {
ODKStorage.createODKDirs();
} catch (RuntimeException e) {
Logger.exception(e);
UserfacingErrorHandling.createErrorDialog(this, e.getMessage(), FormEntryConstants.EXIT);
return;
}
uiController.setupUI();
mGestureDetector = new GestureDetector(this, this);
// needed to override rms property manager
PropertyManager.setPropertyManager(new AndroidPropertyManager(getApplicationContext()));
if (savedInstanceState == null) {
GoogleAnalyticsUtils.reportLanguageAtPointOfFormEntry(Localization.getCurrentLocale());
} else {
loadStateFromBundle(savedInstanceState);
}
// Check to see if this is a screen flip or a new form load.
Object data = this.getLastCustomNonConfigurationInstance();
if (data instanceof FormLoaderTask) {
mFormLoaderTask = (FormLoaderTask)data;
} else if (data instanceof SaveToDiskTask) {
mSaveToDiskTask = (SaveToDiskTask)data;
mSaveToDiskTask.setFormSavedListener(this);
} else if (hasFormLoadBeenTriggered && !hasFormLoadFailed) {
// Screen orientation change
uiController.refreshView();
}
}
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
/*
* EventLog accepts only proper Strings as input, but prior to this version,
* Android would try to send SpannedStrings to it, thus crashing the app.
* This makes sure the title is actually a String.
* This fixes bug 174626.
*/
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2
&& item.getTitleCondensed() != null) {
item.setTitleCondensed(item.getTitleCondensed().toString());
}
return super.onMenuItemSelected(featureId, item);
}
@Override
public void formSaveCallback() {
// note that we have started saving the form
savingFormOnKeySessionExpiration = true;
// start saving form, which will call the key session logout completion
// function when it finishes.
saveIncompleteFormToDisk();
}
private void registerFormEntryReceiver() {
//BroadcastReceiver for:
// a) An unresolvable xpath expression encountered in PollSensorAction.onLocationChanged
// b) Checking if GPS services are not available
mLocationServiceIssueReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
context.removeStickyBroadcast(intent);
badLocationXpath = intent.getStringExtra(PollSensorAction.KEY_UNRESOLVED_XPATH);
locationRecieverErrorAction = intent.getAction();
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(PollSensorAction.XPATH_ERROR_ACTION);
filter.addAction(GeoUtils.ACTION_CHECK_GPS_ENABLED);
registerReceiver(mLocationServiceIssueReceiver, filter);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
instanceState.saveState(outState);
outState.putBoolean(KEY_FORM_LOAD_HAS_TRIGGERED, hasFormLoadBeenTriggered);
outState.putBoolean(KEY_FORM_LOAD_FAILED, hasFormLoadFailed);
outState.putString(KEY_LOC_ERROR, locationRecieverErrorAction);
outState.putString(KEY_LOC_ERROR_PATH, badLocationXpath);
outState.putString(KEY_FORM_CONTENT_URI, formProviderContentURI.toString());
outState.putString(KEY_INSTANCE_CONTENT_URI, instanceProviderContentURI.toString());
outState.putBoolean(KEY_INCOMPLETE_ENABLED, mIncompleteEnabled);
outState.putBoolean(KEY_HAS_SAVED, hasSaved);
outState.putString(KEY_RESIZING_ENABLED, ResizingImageView.resizeMethod);
formEntryRestoreSession.saveFormEntrySession(outState);
if (indexOfWidgetWithVideoPlaying != -1) {
outState.putInt(KEY_WIDGET_WITH_VIDEO_PLAYING, indexOfWidgetWithVideoPlaying);
outState.putInt(KEY_POSITION_OF_VIDEO_PLAYING, positionOfVideoProgress);
}
if (symetricKey != null) {
try {
outState.putString(KEY_AES_STORAGE_KEY, new Base64Wrapper().encodeToString(symetricKey.getEncoded()));
} catch (ClassNotFoundException e) {
// we can't really get here anyway, since we couldn't have decoded the string to begin with
throw new RuntimeException("Base 64 encoding unavailable! Can't pass storage key");
}
}
uiController.saveInstanceState(outState);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
if (requestCode == FormEntryConstants.FORM_PREFERENCES_KEY) {
uiController.refreshCurrentView(false);
return;
}
if (resultCode == RESULT_CANCELED) {
if (requestCode == FormEntryConstants.HIERARCHY_ACTIVITY_FIRST_START) {
// They pressed 'back' on the first hierarchy screen, so we should assume they want
// to back out of form entry all together
finishReturnInstance(false);
} else if (requestCode == FormEntryConstants.INTENT_CALLOUT) {
processIntentResponse(intent, true);
Toast.makeText(this, Localization.get("intent.callout.cancelled"), Toast.LENGTH_SHORT).show();
}
// request was canceled, so do nothing
return;
}
switch (requestCode) {
case FormEntryConstants.INTENT_CALLOUT:
if (!processIntentResponse(intent, false)) {
Toast.makeText(this, Localization.get("intent.callout.unable.to.process"), Toast.LENGTH_SHORT).show();
}
break;
case FormEntryConstants.IMAGE_CAPTURE:
ImageCaptureProcessing.processCaptureResponse(this, FormEntryInstanceState.getInstanceFolder(), true);
break;
case FormEntryConstants.SIGNATURE_CAPTURE:
boolean saved = ImageCaptureProcessing.processCaptureResponse(this, FormEntryInstanceState.getInstanceFolder(), false);
if (saved && !uiController.questionsView.isQuestionList()) {
// attempt to auto-advance if a signature was captured
advance();
}
break;
case FormEntryConstants.IMAGE_CHOOSER:
ImageCaptureProcessing.processImageChooserResponse(this, FormEntryInstanceState.getInstanceFolder(), intent);
break;
case FormEntryConstants.AUDIO_VIDEO_FETCH:
processChooserResponse(intent);
break;
case FormEntryConstants.LOCATION_CAPTURE:
String sl = intent.getStringExtra(FormEntryConstants.LOCATION_RESULT);
uiController.questionsView.setBinaryData(sl, mFormController);
saveAnswersForCurrentScreen(FormEntryConstants.DO_NOT_EVALUATE_CONSTRAINTS);
break;
case FormEntryConstants.HIERARCHY_ACTIVITY:
case FormEntryConstants.HIERARCHY_ACTIVITY_FIRST_START:
if (resultCode == FormHierarchyActivity.RESULT_XPATH_ERROR) {
finish();
} else {
// We may have jumped to a new index in hierarchy activity, so refresh
uiController.refreshCurrentView(false);
}
break;
}
}
public void saveImageWidgetAnswer(ContentValues values) {
Uri imageURI =
getContentResolver().insert(Images.Media.EXTERNAL_CONTENT_URI, values);
Log.i(TAG, "Inserting image returned uri = " + imageURI);
uiController.questionsView.setBinaryData(imageURI, mFormController);
saveAnswersForCurrentScreen(FormEntryConstants.DO_NOT_EVALUATE_CONSTRAINTS);
uiController.refreshView();
}
private void processChooserResponse(Intent intent) {
// 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();
String binaryPath = UriToFilePath.getPathFromUri(CommCareApplication.instance(), media);
if (!FormUploadUtil.isSupportedMultimediaFile(binaryPath)) {
// don't let the user select a file that won't be included in the
// upload to the server
uiController.questionsView.clearAnswer();
Toast.makeText(FormEntryActivity.this,
Localization.get("form.attachment.invalid"),
Toast.LENGTH_LONG).show();
} else {
uiController.questionsView.setBinaryData(media, mFormController);
}
saveAnswersForCurrentScreen(FormEntryConstants.DO_NOT_EVALUATE_CONSTRAINTS);
uiController.refreshView();
}
/**
* Search the the current view's widgets for one that has registered a
* pending callout with the form controller
*/
public QuestionWidget getPendingWidget() {
if (mFormController != null) {
FormIndex pendingIndex = mFormController.getPendingCalloutFormIndex();
if (pendingIndex == null) {
return null;
}
for (QuestionWidget q : uiController.questionsView.getWidgets()) {
if (q.getFormId().equals(pendingIndex)) {
return q;
}
}
Logger.log(AndroidLogger.SOFT_ASSERT,
"getPendingWidget couldn't find question widget with a form index that " +
"matches the pending callout.");
}
return null;
}
/**
* @return Was answer set from intent?
*/
private boolean processIntentResponse(Intent response, boolean wasIntentCancelled) {
// keep track of whether we should auto advance
boolean wasAnswerSet = false;
boolean isQuick = false;
IntentWidget pendingIntentWidget = (IntentWidget)getPendingWidget();
if (pendingIntentWidget != null) {
// get the original intent callout
IntentCallout ic = pendingIntentWidget.getIntentCallout();
if (!wasIntentCancelled) {
isQuick = "quick".equals(ic.getAppearance());
TreeReference context = null;
if (mFormController.getPendingCalloutFormIndex() != null) {
context = mFormController.getPendingCalloutFormIndex().getReference();
}
if (pendingIntentWidget instanceof BarcodeWidget) {
String scanResult = response.getStringExtra("SCAN_RESULT");
if (scanResult != null) {
ic.processBarcodeResponse(context, scanResult);
wasAnswerSet = true;
}
} else {
// Set our instance destination for binary data if needed
String destination = FormEntryInstanceState.getInstanceFolder();
wasAnswerSet = ic.processResponse(response, context, new File(destination));
}
}
if (wasIntentCancelled) {
mFormController.setPendingCalloutAsCancelled();
}
}
// auto advance if we got a good result and are in quick mode
if (wasAnswerSet && isQuick) {
uiController.showNextView();
} else {
uiController.refreshView();
}
return wasAnswerSet;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
if (CommCareApplication.instance().isConsumerApp()) {
// Do not show options menu at all if this is a consumer app
return super.onPrepareOptionsMenu(menu);
}
GoogleAnalyticsUtils.reportOptionsMenuEntry(GoogleAnalyticsFields.CATEGORY_FORM_ENTRY);
menu.removeItem(FormEntryConstants.MENU_LANGUAGES);
menu.removeItem(FormEntryConstants.MENU_HIERARCHY_VIEW);
menu.removeItem(FormEntryConstants.MENU_SAVE);
menu.removeItem(FormEntryConstants.MENU_PREFERENCES);
if (mIncompleteEnabled) {
menu.add(0, FormEntryConstants.MENU_SAVE, 0, StringUtils.getStringRobust(this, R.string.save_all_answers)).setIcon(
android.R.drawable.ic_menu_save);
}
menu.add(0, FormEntryConstants.MENU_HIERARCHY_VIEW, 0, StringUtils.getStringRobust(this, R.string.view_hierarchy)).setIcon(
R.drawable.ic_menu_goto);
boolean hasMultipleLanguages =
(!(mFormController == null || mFormController.getLanguages() == null || mFormController.getLanguages().length == 1));
menu.add(0, FormEntryConstants.MENU_LANGUAGES, 0, StringUtils.getStringRobust(this, R.string.change_language))
.setIcon(R.drawable.ic_menu_start_conversation)
.setEnabled(hasMultipleLanguages);
menu.add(0, FormEntryConstants.MENU_PREFERENCES, 0, StringUtils.getStringRobust(this, R.string.form_entry_settings)).setIcon(
android.R.drawable.ic_menu_preferences);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Map<Integer, String> menuIdToAnalyticsEventLabel = createMenuItemToEventMapping();
GoogleAnalyticsUtils.reportOptionsMenuItemEntry(
GoogleAnalyticsFields.CATEGORY_FORM_ENTRY,
menuIdToAnalyticsEventLabel.get(item.getItemId()));
switch (item.getItemId()) {
case FormEntryConstants.MENU_LANGUAGES:
FormEntryDialogs.createLanguageDialog(this);
return true;
case FormEntryConstants.MENU_SAVE:
saveFormToDisk(FormEntryConstants.DO_NOT_EXIT);
return true;
case FormEntryConstants.MENU_HIERARCHY_VIEW:
if (currentPromptIsQuestion()) {
saveAnswersForCurrentScreen(FormEntryConstants.DO_NOT_EVALUATE_CONSTRAINTS);
}
Intent i = new Intent(this, FormHierarchyActivity.class);
startActivityForResult(i, FormEntryConstants.HIERARCHY_ACTIVITY);
return true;
case FormEntryConstants.MENU_PREFERENCES:
Intent pref = new Intent(this, FormEntryPreferences.class);
startActivityForResult(pref, FormEntryConstants.FORM_PREFERENCES_KEY);
return true;
case android.R.id.home:
GoogleAnalyticsUtils.reportFormQuitAttempt(GoogleAnalyticsFields.LABEL_NAV_BAR_ARROW);
triggerUserQuitInput();
return true;
}
return super.onOptionsItemSelected(item);
}
private static Map<Integer, String> createMenuItemToEventMapping() {
Map<Integer, String> menuIdToAnalyticsEvent = new HashMap<>();
menuIdToAnalyticsEvent.put(FormEntryConstants.MENU_LANGUAGES, GoogleAnalyticsFields.LABEL_CHANGE_LANGUAGE);
menuIdToAnalyticsEvent.put(FormEntryConstants.MENU_SAVE, GoogleAnalyticsFields.LABEL_SAVE_FORM);
menuIdToAnalyticsEvent.put(FormEntryConstants.MENU_HIERARCHY_VIEW, GoogleAnalyticsFields.LABEL_FORM_HIERARCHY);
menuIdToAnalyticsEvent.put(FormEntryConstants.MENU_PREFERENCES, GoogleAnalyticsFields.LABEL_CHANGE_SETTINGS);
return menuIdToAnalyticsEvent;
}
/**
* @return true If the current index of the form controller contains questions
*/
protected boolean currentPromptIsQuestion() {
return (mFormController.getEvent() == FormEntryController.EVENT_QUESTION || mFormController
.getEvent() == FormEntryController.EVENT_GROUP);
}
public boolean saveAnswersForCurrentScreen(boolean evaluateConstraints) {
return saveAnswersForCurrentScreen(evaluateConstraints, true, false);
}
/**
* Attempt to save the answer(s) in the current screen to into the data model.
*
* @param failOnRequired Whether or not the constraint evaluation
* should return false if the question is only
* required. (this is helpful for incomplete
* saves)
* @param headless running in a process that can't display graphics
* @return false if any error occurs while saving (constraint violated,
* etc...), true otherwise.
*/
private boolean saveAnswersForCurrentScreen(boolean evaluateConstraints,
boolean failOnRequired,
boolean headless) {
// only try to save if the current event is a question or a field-list
// group
boolean success = true;
if (isEventQuestionOrListGroup()) {
HashMap<FormIndex, IAnswerData> answers =
uiController.questionsView.getAnswers();
// Sort the answers so if there are multiple errors, we can
// bring focus to the first one
List<FormIndex> indexKeys = new ArrayList<>();
indexKeys.addAll(answers.keySet());
Collections.sort(indexKeys, new Comparator<FormIndex>() {
@Override
public int compare(FormIndex arg0, FormIndex arg1) {
return arg0.compareTo(arg1);
}
});
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);
if (evaluateConstraints &&
((saveStatus != FormEntryController.ANSWER_OK) &&
(failOnRequired ||
saveStatus != FormEntryController.ANSWER_REQUIRED_BUT_EMPTY))) {
if (!headless) {
uiController.showConstraintWarning(index, mFormController.getQuestionPrompt(index).getConstraintText(), saveStatus, success);
}
success = false;
}
} else {
Log.w(TAG,
"Attempted to save an index referencing something other than a question: "
+ index.getReference());
}
}
}
return success;
}
private boolean isEventQuestionOrListGroup() {
return (mFormController.getEvent() == FormEntryController.EVENT_QUESTION) ||
(mFormController.getEvent() == FormEntryController.EVENT_GROUP
&& mFormController.indexIsInFieldList());
}
/**
* Clears the answer on the screen.
*/
public 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, StringUtils.getStringSpannableRobust(this, R.string.clear_answer));
menu.setHeaderTitle(StringUtils.getStringSpannableRobust(this, 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 : uiController.questionsView.getWidgets()) {
if (item.getItemId() == qw.getId()) {
FormEntryDialogs.createClearDialog(this, qw);
}
}
return super.onContextItemSelected(item);
}
/**
* If we're loading, then we pass the loading thread to our next instance.
*/
@Override
public Object onRetainCustomNonConfigurationInstance() {
// 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(FormEntryConstants.DO_NOT_EVALUATE_CONSTRAINTS);
}
return null;
}
@SuppressLint("NewApi")
@Override
public boolean dispatchTouchEvent(MotionEvent mv) {
//We need to ignore this even if it's processed by the action
//bar (if one exists)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
ActionBar bar = getActionBar();
if (bar != null) {
View customView = bar.getCustomView();
if (customView != null && customView.dispatchTouchEvent(mv)) {
return true;
}
}
}
boolean handled = mGestureDetector.onTouchEvent(mv);
return handled || super.dispatchTouchEvent(mv);
}
protected void fireCompoundIntentDispatch() {
CompoundIntentList i = uiController.questionsView.getAggregateIntentCallout();
if (i == null) {
uiController.hideCompoundIntentCalloutButton();
Log.e(TAG, "Multiple intent dispatch button shouldn't have been shown");
return;
}
// We don't process the result on this yet, but Android won't maintain the backstack
// state for the current activity unless it thinks we're going to process the callout
// result.
this.startActivityForResult(i.getCompoundedIntent(), FormEntryConstants.INTENT_COMPOUND_CALLOUT);
}
public void saveFormToDisk(boolean exit) {
if (formHasLoaded()) {
boolean isFormComplete = FormEntryInstanceState.isInstanceComplete(this, instanceProviderContentURI);
saveDataToDisk(exit, isFormComplete, null, false);
} else if (exit) {
showSaveErrorAndExit();
}
}
private void saveCompletedFormToDisk(String updatedSaveName) {
saveDataToDisk(FormEntryConstants.EXIT, true, updatedSaveName, false);
}
private void saveIncompleteFormToDisk() {
saveDataToDisk(FormEntryConstants.EXIT, false, null, true);
}
private void showSaveErrorAndExit() {
Toast.makeText(this, Localization.get("form.entry.save.error"), Toast.LENGTH_SHORT).show();
hasSaved = false;
finishReturnInstance();
}
/**
* Saves form data to disk.
*
* @param exit If set, will exit program after save.
* @param complete Has the user marked the instances as complete?
* @param updatedSaveName Set name of the instance's content provider, if
* non-null
* @param headless Disables GUI warnings and lets answers that
* violate constraints be saved.
*/
private void saveDataToDisk(boolean exit, boolean complete, String updatedSaveName, boolean headless) {
if (!formHasLoaded()) {
if (exit) {
showSaveErrorAndExit();
}
return;
}
// save current answer; if headless, don't evaluate the constraints
// before doing so.
boolean wasScreenSaved =
saveAnswersForCurrentScreen(FormEntryConstants.DO_NOT_EVALUATE_CONSTRAINTS, complete, headless);
if (!wasScreenSaved) {
return;
}
// If a save task is already running, just let it do its thing
if ((mSaveToDiskTask != null) &&
(mSaveToDiskTask.getStatus() != AsyncTask.Status.FINISHED)) {
return;
}
mSaveToDiskTask =
new SaveToDiskTask(getIntent().getData(), exit, complete, updatedSaveName, this, instanceProviderContentURI, symetricKey, headless);
if (!headless) {
mSaveToDiskTask.connect(this);
}
mSaveToDiskTask.setFormSavedListener(this);
mSaveToDiskTask.executeParallel();
}
public void discardChangesAndExit() {
FormFileSystemHelpers.removeMediaAttachedToUnsavedForm(this,
FormEntryInstanceState.mInstancePath, instanceProviderContentURI);
finishReturnInstance(false);
}
public void setFormLanguage(String[] languages, int index) {
// Update the language in the content provider when selecting a new
// language
ContentValues values = new ContentValues();
values.put(FormsColumns.LANGUAGE, languages[index]);
String selection = FormsColumns.FORM_FILE_PATH + "=?";
String selectArgs[] = {
instanceState.getFormPath()
};
int updated =
getContentResolver().update(formProviderContentURI, values,
selection, selectArgs);
Log.i(TAG, "Updated language to: " + languages[index] + " in "
+ updated + " rows");
mFormController.setLanguage(languages[index]);
dismissAlertDialog();
if (currentPromptIsQuestion()) {
saveAnswersForCurrentScreen(FormEntryConstants.DO_NOT_EVALUATE_CONSTRAINTS);
}
uiController.refreshView();
}
@Override
public CustomProgressDialog generateProgressDialog(int id) {
CustomProgressDialog dialog = null;
switch (id) {
case FormLoaderTask.FORM_LOADER_TASK_ID:
dialog = CustomProgressDialog.newInstance(
StringUtils.getStringRobust(this, R.string.loading_form),
StringUtils.getStringRobust(this, R.string.please_wait),
id);
dialog.addCancelButton();
break;
case SaveToDiskTask.SAVING_TASK_ID:
dialog = CustomProgressDialog.newInstance(
StringUtils.getStringRobust(this, R.string.saving_form),
StringUtils.getStringRobust(this, R.string.please_wait),
id);
break;
}
return dialog;
}
@Override
public void taskCancelled() {
finish();
}
@Override
protected void onPause() {
super.onPause();
if (!isFinishing() && uiController.questionsView != null && currentPromptIsQuestion()) {
saveAnswersForCurrentScreen(FormEntryConstants.DO_NOT_EVALUATE_CONSTRAINTS);
}
if (mLocationServiceIssueReceiver != null) {
try {
unregisterReceiver(mLocationServiceIssueReceiver);
} catch (IllegalArgumentException e) {
// Thrown when given receiver isn't registered.
// This shouldn't ever happen, but seems to come up in production
Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION, e.getMessage());
}
}
saveInlineVideoState();
if (isFinishing()) {
PollSensorController.INSTANCE.stopLocationPolling();
}
}
private void saveInlineVideoState() {
if (uiController.questionsView != null) {
for (int i = 0; i < uiController.questionsView.getWidgets().size(); i++) {
QuestionWidget q = uiController.questionsView.getWidgets().get(i);
if (q.findViewById(MediaLayout.INLINE_VIDEO_PANE_ID) != null) {
VideoView inlineVideo = (VideoView)q.findViewById(MediaLayout.INLINE_VIDEO_PANE_ID);
if (inlineVideo.isPlaying()) {
indexOfWidgetWithVideoPlaying = i;
positionOfVideoProgress = inlineVideo.getCurrentPosition();
return;
}
}
}
}
}
private void restoreInlineVideoState() {
if (indexOfWidgetWithVideoPlaying != -1) {
QuestionWidget widgetWithVideoToResume = uiController.questionsView.getWidgets().get(indexOfWidgetWithVideoPlaying);
VideoView inlineVideo = (VideoView)widgetWithVideoToResume.findViewById(MediaLayout.INLINE_VIDEO_PANE_ID);
if (inlineVideo != null) {
inlineVideo.seekTo(positionOfVideoProgress);
inlineVideo.start();
} else {
Logger.log(AndroidLogger.SOFT_ASSERT,
"No inline video was found at the question widget index for which a " +
"video had been playing before the activity was paused");
}
// Reset values now that we have restored
indexOfWidgetWithVideoPlaying = -1;
positionOfVideoProgress = -1;
}
}
@Override
protected void onResumeSessionSafe() {
if (!hasFormLoadBeenTriggered) {
loadForm();
}
registerFormEntryReceiver();
restorePriorStates();
if (mFormController != null) {
mFormController.setPendingCalloutFormIndex(null);
}
}
private void restorePriorStates() {
if (uiController.questionsView != null) {
uiController.questionsView.restoreTimePickerData();
uiController.restoreFocusToCalloutQuestion();
restoreInlineVideoState();
}
}
private void loadForm() {
mFormController = null;
FormEntryInstanceState.mInstancePath = null;
Intent intent = getIntent();
if (intent != null) {
loadIntentFormData(intent);
setTitleToLoading();
Uri uri = intent.getData();
final String contentType = getContentResolver().getType(uri);
Uri formUri;
if (contentType == null) {
UserfacingErrorHandling.createErrorDialog(this, "form URI resolved to null", FormEntryConstants.EXIT);
return;
}
boolean isInstanceReadOnly = false;
try {
switch (contentType) {
case InstanceColumns.CONTENT_ITEM_TYPE:
Pair<Uri, Boolean> instanceAndStatus = FormEntryInstanceState.getInstanceUri(this, uri, formProviderContentURI, instanceState);
formUri = instanceAndStatus.first;
isInstanceReadOnly = instanceAndStatus.second;
break;
case FormsColumns.CONTENT_ITEM_TYPE:
formUri = uri;
instanceState.setFormPath(FormFileSystemHelpers.getFormPath(this, uri));
break;
default:
Log.e(TAG, "unrecognized URI");
UserfacingErrorHandling.createErrorDialog(this, "unrecognized URI: " + uri, FormEntryConstants.EXIT);
return;
}
} catch (FormQueryException e) {
UserfacingErrorHandling.createErrorDialog(this, e.getMessage(), FormEntryConstants.EXIT);
return;
}
if (formUri == null) {
Log.e(TAG, "unrecognized URI");
UserfacingErrorHandling.createErrorDialog(this, "couldn't locate FormDB entry for the item at: " + uri, FormEntryConstants.EXIT);
return;
}
mFormLoaderTask = new FormLoaderTask<FormEntryActivity>(symetricKey, isInstanceReadOnly, formEntryRestoreSession.isRecording(), this) {
@Override
protected void deliverResult(FormEntryActivity receiver, FECWrapper wrapperResult) {
receiver.handleFormLoadCompletion(wrapperResult.getController());
}
@Override
protected void deliverUpdate(FormEntryActivity receiver, String... update) {
}
@Override
protected void deliverError(FormEntryActivity receiver, Exception e) {
receiver.setFormLoadFailure();
receiver.dismissProgressDialog();
if (e != null) {
UserfacingErrorHandling.createErrorDialog(receiver, e.getMessage(), FormEntryConstants.EXIT);
} else {
UserfacingErrorHandling.createErrorDialog(receiver, StringUtils.getStringRobust(receiver, R.string.parse_error), FormEntryConstants.EXIT);
}
}
};
mFormLoaderTask.connect(this);
mFormLoaderTask.executeParallel(formUri);
hasFormLoadBeenTriggered = true;
}
}
private void handleFormLoadCompletion(AndroidFormController fc) {
if (GeoUtils.ACTION_CHECK_GPS_ENABLED.equals(locationRecieverErrorAction)) {
FormEntryDialogs.handleNoGpsBroadcast(this);
} else if (PollSensorAction.XPATH_ERROR_ACTION.equals(locationRecieverErrorAction)) {
handleXpathErrorBroadcast();
}
mFormController = fc;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
// Newer menus may have already built the menu, before all data was ready
invalidateOptionsMenu();
}
registerSessionFormSaveCallback();
// Set saved answer path
if (FormEntryInstanceState.mInstancePath == null) {
instanceState.initInstancePath();
} else {
// we've just loaded a saved form, so start in the hierarchy view
Intent i = new Intent(this, FormHierarchyActivity.class);
startActivityForResult(i, FormEntryConstants.HIERARCHY_ACTIVITY_FIRST_START);
return; // so we don't show the intro screen before jumping to the hierarchy
}
reportFormEntry();
formEntryRestoreSession.replaySession(this);
uiController.refreshView();
FormNavigationUI.updateNavigationCues(this, mFormController, uiController.questionsView);
}
private void handleXpathErrorBroadcast() {
UserfacingErrorHandling.createErrorDialog(FormEntryActivity.this,
"There is a bug in one of your form's XPath Expressions \n" + badLocationXpath, FormEntryConstants.EXIT);
}
/**
* Call when the user provides input that they want to quit the form
*/
protected void triggerUserQuitInput() {
if (!formHasLoaded()) {
finish();
} else if (mFormController.isFormReadOnly()) {
// If we're just reviewing a read only form, don't worry about saving
// or what not, just quit
// It's possible we just want to "finish" here, but
// I don't really wanna break any c compatibility
finishReturnInstance(false);
} else {
FormEntryDialogs.createQuitDialog(this, mIncompleteEnabled);
return;
}
GoogleAnalyticsUtils.reportFormExit(GoogleAnalyticsFields.LABEL_NO_DIALOG);
}
/**
* Call when the user is ready to save and return the current form as complete
*/
protected void triggerUserFormComplete() {
if (mFormController.isFormReadOnly()) {
finishReturnInstance(false);
} else {
saveCompletedFormToDisk(FormEntryInstanceState.getDefaultFormTitle(this, getIntent()));
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
GoogleAnalyticsUtils.reportFormQuitAttempt(GoogleAnalyticsFields.LABEL_DEVICE_BUTTON);
triggerUserQuitInput();
return true;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (event.isAltPressed() && !uiController.shouldIgnoreSwipeAction()) {
uiController.setIsAnimatingSwipe();
uiController.showNextView();
return true;
}
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
if (event.isAltPressed() && !uiController.shouldIgnoreSwipeAction()) {
uiController.setIsAnimatingSwipe();
uiController.showPreviousView(true);
return true;
}
break;
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onDestroy() {
if (mFormLoaderTask != 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) {
// 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();
}
private void registerSessionFormSaveCallback() {
if (mFormController != null && !mFormController.isFormReadOnly()) {
try {
// CommCareSessionService will call this.formSaveCallback when
// the key session is closing down and we need to save any
// intermediate results before they become un-saveable.
CommCareApplication.instance().getSession().registerFormSaveCallback(this);
} catch (SessionUnavailableException e) {
Log.w(TAG,
"Couldn't register form save callback because session doesn't exist");
}
}
}
/**
* {@inheritDoc}
*
* Display save status notification and exit or continue on in the form.
* If form entry is being saved because key session is expiring then
* continue closing the session/logging out.
*/
@Override
public void savingComplete(SaveToDiskTask.SaveStatus saveStatus, String errorMessage) {
// Did we just save a form because the key session
// (CommCareSessionService) is ending?
if (savingFormOnKeySessionExpiration) {
savingFormOnKeySessionExpiration = false;
// Notify the key session that the form state has been saved (or at
// least attempted to be saved) so CommCareSessionService can
// continue closing down key pool and user database.
CommCareApplication.instance().expireUserSession();
} else if (saveStatus != null) {
String toastMessage = "";
switch (saveStatus) {
case SAVED_COMPLETE:
toastMessage = Localization.get("form.entry.complete.save.success");
hasSaved = true;
break;
case SAVED_INCOMPLETE:
toastMessage = Localization.get("form.entry.incomplete.save.success");
hasSaved = true;
break;
case SAVED_AND_EXIT:
toastMessage = Localization.get("form.entry.complete.save.success");
hasSaved = true;
finishReturnInstance();
break;
case INVALID_ANSWER:
// an answer constraint was violated, so try to save the
// current question to trigger the constraint violation message
uiController.refreshView();
saveAnswersForCurrentScreen(FormEntryConstants.EVALUATE_CONSTRAINTS);
return;
case SAVE_ERROR:
if (!CommCareApplication.instance().isConsumerApp()) {
UserfacingErrorHandling.createErrorDialog(this, errorMessage,
Localization.get("notification.formentry.save_error.title"), FormEntryConstants.EXIT);
}
return;
}
if (!"".equals(toastMessage) && !CommCareApplication.instance().isConsumerApp()) {
Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show();
}
uiController.refreshView();
}
}
/**
* Attempts to save an answer to the specified index.
*
* @param evaluateConstraints Should form constraints be checked when saving answer?
* @return status as determined in FormEntryController
*/
private int saveAnswer(IAnswerData answer, FormIndex index, boolean evaluateConstraints) {
try {
if (evaluateConstraints) {
return mFormController.answerQuestion(index, answer);
} else {
mFormController.saveAnswer(index, answer);
return FormEntryController.ANSWER_OK;
}
} catch (XPathException e) {
//this is where runtime exceptions get triggered after the form has loaded
UserfacingErrorHandling.logErrorAndShowDialog(this, e, FormEntryConstants.EXIT);
//We're exiting anyway
return FormEntryController.ANSWER_OK;
}
}
private void finishReturnInstance() {
finishReturnInstance(true);
}
/**
* Returns the instance that was just filled out to the calling activity,
* if requested.
*
* @param reportSaved was a form saved? Delegates the result code of the
* activity
*/
private void finishReturnInstance(boolean reportSaved) {
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 = {
FormEntryInstanceState.mInstancePath
};
Cursor c = null;
try {
c = getContentResolver().query(instanceProviderContentURI, null, selection, selectionArgs, null);
if (c != null && c.getCount() > 0) {
// should only be one...
c.moveToFirst();
String id = c.getString(c.getColumnIndex(InstanceColumns._ID));
Uri instance = Uri.withAppendedPath(instanceProviderContentURI, id);
Intent formReturnIntent = new Intent();
formReturnIntent.putExtra(FormEntryConstants.IS_ARCHIVED_FORM, mFormController.isFormReadOnly());
if (reportSaved || hasSaved) {
setResult(RESULT_OK, formReturnIntent.setData(instance));
} else {
setResult(RESULT_CANCELED, formReturnIntent.setData(instance));
}
}
} finally {
if (c != null) {
c.close();
}
}
}
try {
CommCareApplication.instance().getSession().unregisterFormSaveCallback();
} catch (SessionUnavailableException sue) {
// looks like the session expired, swallow exception because we
// might be auto-saving a form due to user session expiring
}
dismissProgressDialog();
reportFormExit();
finish();
}
@Override
protected boolean onBackwardSwipe() {
GoogleAnalyticsUtils.reportFormNavBackward(GoogleAnalyticsFields.LABEL_SWIPE);
uiController.showPreviousView(true);
return true;
}
@Override
protected boolean onForwardSwipe() {
if (canNavigateForward()) {
GoogleAnalyticsUtils.reportFormNavForward(
GoogleAnalyticsFields.LABEL_SWIPE,
GoogleAnalyticsFields.VALUE_FORM_NOT_DONE);
uiController.next();
return true;
} else {
GoogleAnalyticsUtils.reportFormNavForward(
GoogleAnalyticsFields.LABEL_SWIPE,
GoogleAnalyticsFields.VALUE_FORM_DONE);
FormNavigationUI.animateFinishArrow(this);
return true;
}
}
@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 (uiController.questionsView != null) {
uiController.questionsView.cancelLongPress();
}
return false;
}
@Override
public void advance() {
if (canNavigateForward()) {
uiController.next();
}
}
@Override
public void widgetEntryChanged(QuestionWidget changedWidget) {
try {
uiController.recordLastChangedWidgetIndex(changedWidget);
uiController.updateFormRelevancies();
} catch (XPathTypeMismatchException e) {
UserfacingErrorHandling.logErrorAndShowDialog(this, e, FormEntryConstants.EXIT);
return;
}
FormNavigationUI.updateNavigationCues(this, mFormController, uiController.questionsView);
}
private boolean canNavigateForward() {
ImageButton nextButton = (ImageButton)this.findViewById(R.id.nav_btn_next);
return FormEntryConstants.NAV_STATE_NEXT.equals(nextButton.getTag());
}
/**
* Has form loading (via FormLoaderTask) completed?
*/
private boolean formHasLoaded() {
return mFormController != null;
}
private void loadStateFromBundle(Bundle savedInstanceState) {
if (savedInstanceState != null) {
instanceState.loadState(savedInstanceState);
if (savedInstanceState.containsKey(KEY_FORM_LOAD_HAS_TRIGGERED)) {
hasFormLoadBeenTriggered = savedInstanceState.getBoolean(KEY_FORM_LOAD_HAS_TRIGGERED, false);
}
if (savedInstanceState.containsKey(KEY_FORM_LOAD_FAILED)) {
hasFormLoadFailed = savedInstanceState.getBoolean(KEY_FORM_LOAD_FAILED, false);
}
locationRecieverErrorAction = savedInstanceState.getString(KEY_LOC_ERROR);
badLocationXpath = savedInstanceState.getString(KEY_LOC_ERROR_PATH);
if (savedInstanceState.containsKey(KEY_FORM_CONTENT_URI)) {
formProviderContentURI = Uri.parse(savedInstanceState.getString(KEY_FORM_CONTENT_URI));
}
if (savedInstanceState.containsKey(KEY_INSTANCE_CONTENT_URI)) {
instanceProviderContentURI = Uri.parse(savedInstanceState.getString(KEY_INSTANCE_CONTENT_URI));
}
if (savedInstanceState.containsKey(KEY_INCOMPLETE_ENABLED)) {
mIncompleteEnabled = savedInstanceState.getBoolean(KEY_INCOMPLETE_ENABLED);
}
if (savedInstanceState.containsKey(KEY_RESIZING_ENABLED)) {
ResizingImageView.resizeMethod = savedInstanceState.getString(KEY_RESIZING_ENABLED);
}
if (savedInstanceState.containsKey(KEY_AES_STORAGE_KEY)) {
String base64Key = savedInstanceState.getString(KEY_AES_STORAGE_KEY);
try {
byte[] storageKey = new Base64Wrapper().decode(base64Key);
symetricKey = new SecretKeySpec(storageKey, "AES");
} catch (ClassNotFoundException e) {
throw new RuntimeException("Base64 encoding not available on this platform");
}
}
if (savedInstanceState.containsKey(KEY_HEADER_STRING)) {
mHeaderString = savedInstanceState.getString(KEY_HEADER_STRING);
}
if (savedInstanceState.containsKey(KEY_HAS_SAVED)) {
hasSaved = savedInstanceState.getBoolean(KEY_HAS_SAVED);
}
formEntryRestoreSession.restoreFormEntrySession(savedInstanceState,
CommCareApplication.instance().getPrototypeFactory(this));
if (savedInstanceState.containsKey(KEY_WIDGET_WITH_VIDEO_PLAYING)) {
indexOfWidgetWithVideoPlaying = savedInstanceState.getInt(KEY_WIDGET_WITH_VIDEO_PLAYING);
positionOfVideoProgress = savedInstanceState.getInt(KEY_POSITION_OF_VIDEO_PLAYING);
}
uiController.restoreSavedState(savedInstanceState);
}
}
private void loadIntentFormData(Intent intent) {
if (intent.hasExtra(KEY_FORM_CONTENT_URI)) {
this.formProviderContentURI = Uri.parse(intent.getStringExtra(KEY_FORM_CONTENT_URI));
}
if (intent.hasExtra(KEY_INSTANCE_CONTENT_URI)) {
this.instanceProviderContentURI = Uri.parse(intent.getStringExtra(KEY_INSTANCE_CONTENT_URI));
}
instanceState.loadFromIntent(intent);
if (intent.hasExtra(KEY_AES_STORAGE_KEY)) {
String base64Key = intent.getStringExtra(KEY_AES_STORAGE_KEY);
try {
byte[] storageKey = new Base64Wrapper().decode(base64Key);
symetricKey = new SecretKeySpec(storageKey, "AES");
} catch (ClassNotFoundException e) {
throw new RuntimeException("Base64 encoding not available on this platform");
}
}
if (intent.hasExtra(KEY_HEADER_STRING)) {
FormEntryActivity.mHeaderString = intent.getStringExtra(KEY_HEADER_STRING);
}
if (intent.hasExtra(KEY_INCOMPLETE_ENABLED)) {
this.mIncompleteEnabled = intent.getBooleanExtra(KEY_INCOMPLETE_ENABLED, true);
}
if (intent.hasExtra(KEY_RESIZING_ENABLED)) {
ResizingImageView.resizeMethod = intent.getStringExtra(KEY_RESIZING_ENABLED);
}
formEntryRestoreSession.loadFromIntent(intent);
}
private void setTitleToLoading() {
if (mHeaderString != null) {
setTitle(mHeaderString);
} else {
setTitle(StringUtils.getStringRobust(this, R.string.application_name) + " > " + StringUtils.getStringRobust(this, R.string.loading_form));
}
}
protected String getHeaderString() {
if (mHeaderString != null) {
//Localization?
return mHeaderString;
} else {
return StringUtils.getStringRobust(this, R.string.application_name) + " > " + FormEntryActivity.mFormController.getFormTitle();
}
}
public static class FormQueryException extends Exception {
public FormQueryException(String msg) {
super(msg);
}
}
private void setFormLoadFailure() {
hasFormLoadFailed = true;
}
@Override
protected void onMajorLayoutChange(Rect newRootViewDimensions) {
uiController.recalcShouldHideGroupLabel(newRootViewDimensions);
}
private void reportFormEntry() {
TimedStatsTracker.registerEnterForm(getCurrentFormID());
}
private void reportFormExit() {
TimedStatsTracker.registerExitForm(getCurrentFormID());
}
private int getCurrentFormID() {
return mFormController.getFormID();
}
/**
* For Testing purposes only
*/
public QuestionsView getODKView() {
if (BuildConfig.DEBUG) {
return uiController.questionsView;
} else {
throw new RuntimeException("On principal of design, only meant for testing purposes");
}
}
public static String getFormEntrySessionString() {
if (mFormController == null) {
return "";
} else {
return mFormController.getFormEntrySessionString();
}
}
@Override
public void initUIController() {
uiController = new FormEntryActivityUIController(this);
}
@Override
public CommCareActivityUIController getUIController() {
return uiController;
}
}