package com.radicaldynamic.gcmobile.android.build;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.ektorp.Attachment;
import org.ektorp.AttachmentInputStream;
import org.odk.collect.android.logic.FormController;
import org.odk.collect.android.utilities.FileUtils;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ListActivity;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.radicaldynamic.groupinform.R;
import com.radicaldynamic.groupinform.activities.FormEntryActivity;
import com.radicaldynamic.groupinform.adapters.FormBuilderFieldListAdapter;
import com.radicaldynamic.groupinform.application.Collect;
import com.radicaldynamic.groupinform.documents.FormDefinition;
import com.radicaldynamic.groupinform.documents.FormInstance;
import com.radicaldynamic.groupinform.listeners.FormLoaderListener;
import com.radicaldynamic.groupinform.listeners.FormSavedListener;
import com.radicaldynamic.groupinform.tasks.SaveToDiskTask;
import com.radicaldynamic.groupinform.utilities.Base64Coder;
import com.radicaldynamic.groupinform.utilities.FileUtilsExtended;
import com.radicaldynamic.groupinform.views.TouchListView;
import com.radicaldynamic.groupinform.xform.Bind;
import com.radicaldynamic.groupinform.xform.Field;
import com.radicaldynamic.groupinform.xform.FormBuilderState;
import com.radicaldynamic.groupinform.xform.FormReader;
import com.radicaldynamic.groupinform.xform.FormWriter;
import com.radicaldynamic.groupinform.xform.Instance;
import com.radicaldynamic.groupinform.xform.FormWriter.FormSanityException;
public class FieldList extends ListActivity implements FormLoaderListener, FormSavedListener
{
private static final String t = "FormBuilderElementList: ";
private static final int LOADING_DIALOG = 1;
private static final int SAVING_DIALOG = 2;
private static final int SANITY_DIALOG = 3;
private static final int REQUEST_EDITFIELD = 1;
private static final int REQUEST_TRANSLATIONS = 2;
private static final String KEY_ACTUALPATH = "actualpath";
private static final String KEY_INSTANCE_ROOT = "instanceroot";
private static final String KEY_INSTANCE_ROOT_ID = "instancerootid";
private static final String KEY_PATH = "path";
private LoadFormDefinitionTask mLoadFormDefinitionTask;
private SaveFormDefinitionTask mSaveFormDefinitionTask;
private AlertDialog mAlertDialog;
private String mAlertDialogMsg;
private ProgressDialog mProgressDialog;
private FormBuilderFieldListAdapter mAdapter = null;
private Button jumpPreviousButton;
private TextView mPathText;
private String mFormId;
private String mInstanceRoot; // XForm instance root tag name
private String mInstanceRootId; // Value of XForm instance root tag id attribute
private FormDefinition mForm;
private FormReader mFormReader;
private ArrayList<Field> mFieldState = new ArrayList<Field>();
private ArrayList<String> mPath = new ArrayList<String>(); // Human readable location in mFieldState
private ArrayList<String> mActualPath = new ArrayList<String>(); // Actual location in mFieldState
/*
* FIXME: element icons are not kept consistent when list items are reordered.
* I am not sure whether this affects only the items that are actually moved
* or the ones that are next to them.
*/
private TouchListView.DropListener onDrop = new TouchListView.DropListener() {
@Override
public void drop(int from, int to)
{
Field item = mAdapter.getItem(from);
mAdapter.remove(item);
mAdapter.insert(item, to);
}
};
private TouchListView.RemoveListener onRemove = new TouchListView.RemoveListener() {
@Override
public void remove(int which)
{
final Field item = mAdapter.getItem(which);
mAlertDialog = new AlertDialog.Builder(FieldList.this)
.setCancelable(false)
.setIcon(R.drawable.ic_dialog_alert)
.setTitle(R.string.tf_confirm_removal)
.setMessage(getString(R.string.tf_confirm_removal_msg, item.getLabel().toString()))
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
removeItem(item);
// Trigger a refresh of the view (and display any pertinent messages)
if (mAdapter.isEmpty())
refreshView();
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
})
.create();
mAlertDialog.show();
}
private void displayRemovalFailed(String msg)
{
mAlertDialog = new AlertDialog.Builder(FieldList.this)
.setIcon(R.drawable.ic_dialog_alert)
.setTitle(R.string.tf_unable_to_remove)
.setMessage(msg)
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
})
.create();
mAlertDialog.show();
}
private void displayRemovedMsg(String msg)
{
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
}
private void removeItem(Field item)
{
/*
* Group removal must be dealt with separately from regular items and repeated groups differ from regular groups.
* This code takes the easy way out and refuses to remove groups that are not empty. This saves us from having to
* worry about recursive removal of binds and instances.
*/
if (item.getType().equals("group")) {
if (Field.isRepeatedGroup(item)) {
if (item.getRepeat().getChildren().isEmpty()) {
removeByXPath(item.getRepeat().getXPath());
} else {
displayRemovalFailed(getString(R.string.tf_removal_failed, item.getLabel().toString()));
return;
}
} else {
if (!item.getChildren().isEmpty()) {
displayRemovalFailed(getString(R.string.tf_removal_failed, item.getLabel().toString()));
return;
}
}
} else {
removeByXPath(item.getXPath());
}
// This removes the (control) field from mFieldState
mAdapter.remove(item);
displayRemovedMsg(getString(R.string.tf_removed_with_param, item.getLabel().toString()));
}
private void removeByXPath(String xpath)
{
if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "removing instances and binds matching XPath " + xpath);
// Also remove the related instance
removeInstanceByXPath(xpath, null);
// Also remove the related bind
Iterator<Bind> it = Collect.getInstance().getFormBuilderState().getBinds().iterator();
while (it.hasNext()) {
Bind bind = it.next();
if (Collect.Log.VERBOSE) Log.v(Collect.LOGTAG, t + "evaluating bind for XPath " + bind.getXPath() + " for removal");
// Remove any binds with an identical XPath to the field in question or those that are logical children
if (bind.getXPath().equals(xpath) || bind.getXPath().matches("^" + xpath + "/*$")) {
if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "removing bind for XPath " + bind.getXPath());
it.remove();
}
}
}
/*
* Iterate through the instances recursively and remove the instance
* (and all children) that match the XPath passed to this method.
*
* This is intended to be called when a (control) field is removed
* from a list.
*/
private void removeInstanceByXPath(String xpath, Instance incomingInstance)
{
Iterator<Instance> it;
if (incomingInstance == null)
it = Collect.getInstance().getFormBuilderState().getInstance().iterator();
else
it = incomingInstance.getChildren().iterator();
while (it.hasNext()) {
Instance i = it.next();
if (Collect.Log.VERBOSE) Log.v(Collect.LOGTAG, t + "evaluating instance with XPath " + i.getXPath() + " for removal");
if (i.getXPath().equals(xpath)) {
if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "removing instance with XPath " + i.getXPath());
it.remove();
return;
}
removeInstanceByXPath(xpath, i);
}
}
};
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.fb_main);
// Needed to manipulate the visual representation of our place in the form
mPathText = (TextView) findViewById(R.id.pathText);
jumpPreviousButton = (Button) findViewById(R.id.jumpPreviousButton);
jumpPreviousButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
goUpLevel();
}
});
if (savedInstanceState == null) {
Intent i = getIntent();
// Load new form definition from scratch
if (i != null) {
mFormId = i.getStringExtra(FormEntryActivity.KEY_FORMPATH);
mLoadFormDefinitionTask = new LoadFormDefinitionTask();
mLoadFormDefinitionTask.setFormLoaderListener(this);
mLoadFormDefinitionTask.execute(mFormId);
showDialog(LOADING_DIALOG);
}
} else {
// Restore state information provided by this activity
if (savedInstanceState.containsKey(FormEntryActivity.KEY_FORMPATH))
mFormId = savedInstanceState.getString(FormEntryActivity.KEY_FORMPATH);
if (savedInstanceState.containsKey(KEY_PATH))
mPath = savedInstanceState.getStringArrayList(KEY_PATH);
if (savedInstanceState.containsKey(KEY_ACTUALPATH))
mActualPath = savedInstanceState.getStringArrayList(KEY_ACTUALPATH);
if (savedInstanceState.containsKey(KEY_INSTANCE_ROOT))
mInstanceRoot = savedInstanceState.getString(KEY_INSTANCE_ROOT);
if (savedInstanceState.containsKey(KEY_INSTANCE_ROOT_ID))
mInstanceRootId = savedInstanceState.getString(KEY_INSTANCE_ROOT_ID);
// Check to see if this is a screen flip or a new form load
Object data = getLastNonConfigurationInstance();
if (data instanceof LoadFormDefinitionTask) {
mLoadFormDefinitionTask = (LoadFormDefinitionTask) data;
} else if (data instanceof SaveFormDefinitionTask) {
mSaveFormDefinitionTask = (SaveFormDefinitionTask) data;
} else if (data == null) {
// Load important bits of the form definition from memory
mFieldState = Collect.getInstance().getFormBuilderState().getFields();
mForm = Collect.getInstance().getFormBuilderState().getFormDefinition();
refreshView();
}
} // end if savedInstanceState == null
}
// Listen for results
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
if (resultCode == RESULT_CANCELED)
return;
switch (requestCode) {
case REQUEST_EDITFIELD:
if (Collect.getInstance().getFormBuilderState().getField().isSaved())
Collect.getInstance().getFormBuilderState().getField().setSaved(false);
// New fields require further init after being saved
if (Collect.getInstance().getFormBuilderState().getField().isNewField())
addNewField(Collect.getInstance().getFormBuilderState().getField());
// Display with the new field included
mFieldState = Collect.getInstance().getFormBuilderState().getFields();
refreshView();
break;
case REQUEST_TRANSLATIONS:
// User may have updated default translations and these need to be reflected on-screen
refreshView();
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, v, menuInfo);
// MenuInflater inflater = getMenuInflater();
// inflater.inflate(R.menu.form_builder_context, menu);
}
/*
* (non-Javadoc)
*
* @see android.app.Activity#onCreateDialog(int)
*/
@Override
protected Dialog onCreateDialog(int id)
{
switch (id) {
case LOADING_DIALOG:
mProgressDialog = new ProgressDialog(this);
mProgressDialog.setMessage(getText(R.string.tf_loading_please_wait));
mProgressDialog.setIndeterminate(true);
mProgressDialog.setCancelable(false);
return mProgressDialog;
case SAVING_DIALOG:
mProgressDialog = new ProgressDialog(this);
mProgressDialog.setMessage(getText(R.string.tf_saving_please_wait));
mProgressDialog.setIndeterminate(true);
mProgressDialog.setCancelable(false);
return mProgressDialog;
case SANITY_DIALOG:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder
.setIcon(R.drawable.ic_dialog_alert)
.setTitle(R.string.tf_form_builder_sanity_dialog)
.setMessage(mAlertDialogMsg);
builder.setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
removeDialog(SANITY_DIALOG);
}
});
return builder.create();
}
return null;
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.formbuilderfieldlist_omenu, menu);
return true;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event)
{
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
createQuitDialog();
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onListItemClick(ListView listView, View view, int position, long id)
{
Field field = (Field) getListAdapter().getItem(position);
/*
* If the form field that has been clicked on is either a group or a repeat the default
* behaviour is to navigate "down" into the form to display elements contained by others.
*
* These form elements may be edited by using a context menu (or possibly using an
* option menu that will become enabled if the user has navigated below the top).
*/
if (field.getType().equals("group") || field.getType().equals("repeat")) {
/*
* So we can find our way back "up" the tree later
*
* Note that the "repeated" portions of repeated groups never become active;
* we just hide this from the user using specific control decisions. This might
* become a candidate for refactoring later, though. It is a bit hairy.
*/
field.setActive(true);
// Deactivate the parent, if applicable
if (field.getParent() != null)
field.getParent().setActive(false);
// Make sure parents of parents are also deactivated (as in the case of nested repeated groups)
if (field.getParent() != null && field.getParent().getParent() != null)
field.getParent().getParent().setActive(false);
if (field.getLabel().toString().length() == 0)
mPath.add("Label missing");
else
mPath.add(field.getLabel().toString());
// Special logic to hide the complexity of repeated groups
if (Field.isRepeatedGroup(field)) {
mActualPath.add(field.getLabel().toString());
mActualPath.add(field.getRepeat().getLabel().toString());
refreshView(field.getRepeat().getChildren());
} else {
mActualPath.add(field.getLabel().toString());
refreshView(field.getChildren());
}
} else {
/*
* There is no case here for groups/repeated groups since this is not how
* we access them via the form builder field editor
*/
String humanFieldType = null;
if (field.getType().equals("input") || field.getType().equals("trigger")) {
if (field.getBind() == null || field.getBind().getType().equals("string")) {
humanFieldType = "text";
} else if (field.getBind().getType().equals("decimal") || field.getBind().getType().equals("int")) {
humanFieldType = "number";
} else {
humanFieldType = field.getBind().getType();
}
} else if (field.getType().equals("select") || field.getType().equals("select1")) {
humanFieldType = "select";
} else if (field.getType().equals("upload")) {
humanFieldType = "media";
} else if (field.getType().equals("trigger")) {
humanFieldType = "trigger";
}
if (humanFieldType != null) {
startFieldEditor(humanFieldType, field);
} else {
Toast.makeText(getApplicationContext(), getString(R.string.tf_unable_to_edit_unknown_field_type), Toast.LENGTH_LONG).show();
}
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId()) {
case R.id.barcode: startFieldEditor("barcode", null); break;
case R.id.date: startFieldEditor("date", null); break;
case R.id.draw: startFieldEditor("draw", null); break; // Note: draw is a virtual type - it will be turned into media once created
case R.id.geopoint: startFieldEditor("geopoint", null); break;
case R.id.group: startFieldEditor("group", null); break;
case R.id.media: startFieldEditor("media", null); break;
case R.id.number: startFieldEditor("number", null); break;
case R.id.select: startFieldEditor("select", null); break;
case R.id.text: startFieldEditor("text", null); break;
case R.id.i18n_setup:
Intent i = new Intent(this, I18nList.class);
startActivityForResult(i, REQUEST_TRANSLATIONS);
break;
case R.id.view_instance:
startActivity(new Intent(this, InstanceList.class));
break;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putString(FormEntryActivity.KEY_FORMPATH, mFormId);
outState.putString(KEY_INSTANCE_ROOT, mInstanceRoot);
outState.putString(KEY_INSTANCE_ROOT_ID, mInstanceRootId);
outState.putStringArrayList(KEY_PATH, mPath);
outState.putStringArrayList(KEY_ACTUALPATH, mActualPath);
}
/*
* Parse and load the form so that it can be displayed and manipulated
*/
private class LoadFormDefinitionTask extends AsyncTask<String, Void, Void>
{
FormLoaderListener mStateListener;
String mError = null;
@Override
protected Void doInBackground(String... args)
{
String formId = args[0];
resetStates();
try {
mForm = Collect.getInstance().getDbService().getDb().get(FormDefinition.class, formId);
Collect.getInstance().getFormBuilderState().setFormDefinition(mForm);
if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "Retrieved form " + mForm.getName() + " from database");
if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "Retreiving form XML from database...");
AttachmentInputStream ais = Collect.getInstance().getDbService().getDb().getAttachment(formId, "xml");
mFormReader = new FormReader(ais, false);
ais.close();
mFieldState = mFormReader.getFields();
mInstanceRoot = mFormReader.getInstanceRoot();
mInstanceRootId = mFormReader.getInstanceRootId();
Collect.getInstance().getFormBuilderState().setBinds(mFormReader.getBinds());
Collect.getInstance().getFormBuilderState().setFields(mFieldState);
Collect.getInstance().getFormBuilderState().setInstance(mFormReader.getInstance());
Collect.getInstance().getFormBuilderState().setTranslations(mFormReader.getTranslations());
} catch (Exception e) {
e.printStackTrace();
mError = e.toString();
}
return null;
}
@Override
protected void onPostExecute(Void nothing)
{
synchronized (this) {
if (mStateListener != null) {
if (mError == null) {
mStateListener.loadingComplete(null, null, null);
} else {
mStateListener.loadingError(mError);
}
}
}
}
public void setFormLoaderListener(FormLoaderListener sl)
{
synchronized (this) {
mStateListener = sl;
}
}
private void resetStates()
{
// States stored in this object
mFieldState = new ArrayList<Field>();
mPath = new ArrayList<String>();
mActualPath = new ArrayList<String>();
// States stored in the global application context
Collect.getInstance().setFormBuilderState(null);
Collect.getInstance().setFormBuilderState(new FormBuilderState());
}
}
/*
* Save the form as-is given the state of the XForm in memory
*/
private class SaveFormDefinitionTask extends AsyncTask<Integer, Void, Integer>
{
private FormSavedListener mSavedListener;
@Override
protected Integer doInBackground(Integer... resultCode)
{
if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "converting form to XML and attaching to database document...");
Integer result = resultCode[0];
try {
// Save to file first so we can get md5 hash
String name = Collect.getInstance().getFormBuilderState().getFormDefinition().getName();
byte[] xml = FormWriter.writeXml(name, mInstanceRoot, mInstanceRootId);
File f = new File(FileUtilsExtended.EXTERNAL_CACHE + File.separator + mForm.getId() + ".xml");
FileOutputStream fos = new FileOutputStream(f);
fos.write(xml);
fos.close();
// Write out XML to database
mForm.addInlineAttachment(new Attachment("xml", new String(Base64Coder.encode(xml)).toString(), FormWriter.CONTENT_TYPE));
mForm.setStatus(FormDefinition.Status.active);
mForm.setXmlHash(FileUtils.getMd5Hash(f));
Collect.getInstance().getDbService().getDb().update(mForm);
f.delete();
} catch (FormSanityException e) {
if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + "sanity exception while writing XForm " + e.toString());
e.printStackTrace();
mAlertDialogMsg = e.getMessage();
result = SaveToDiskTask.VALIDATE_ERROR;
} catch (Exception e) {
if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "failed writing XForm to XML: " + e.toString());
e.printStackTrace();
result = SaveToDiskTask.SAVE_ERROR;
}
return result;
}
@Override
protected void onPostExecute(Integer result) {
synchronized (this) {
if (mSavedListener != null)
mSavedListener.savingComplete(result, null);
}
}
public void setFormSavedListener(FormSavedListener fsl) {
synchronized (this) {
mSavedListener = fsl;
}
}
}
/*
* This is repurposed from the FormLoadListener used for FormEntryActivity and as such
* the FormEntryController parameter has no use here and will be passed a null value.
*
* (non-Javadoc)
* @see com.radicaldynamic.turboform.listeners.FormLoaderListener#loadingComplete
*/
@Override
public void loadingComplete(FormController fec, FormDefinition fdd, FormInstance fid)
{
dismissDialog(LOADING_DIALOG);
refreshView(mFieldState);
}
@Override
public void loadingError(String errorMsg)
{
dismissDialog(LOADING_DIALOG);
mAlertDialog = new AlertDialog.Builder(FieldList.this)
.setCancelable(false)
.setIcon(R.drawable.ic_dialog_alert)
.setTitle(R.string.tf_form_builder_load_error)
.setMessage(errorMsg)
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
finish();
}
})
.create();
mAlertDialog.show();
}
@Override
public void savingComplete(int saveStatus, FormInstance fi)
{
dismissDialog(SAVING_DIALOG);
switch (saveStatus) {
case SaveToDiskTask.VALIDATE_ERROR:
showDialog(SANITY_DIALOG);
break;
case SaveToDiskTask.SAVED:
Toast.makeText(getApplicationContext(), getString(R.string.data_saved_ok), Toast.LENGTH_SHORT).show();
break;
case SaveToDiskTask.SAVED_AND_EXIT:
Toast.makeText(getApplicationContext(), getString(R.string.data_saved_ok), Toast.LENGTH_SHORT).show();
finish();
break;
case SaveToDiskTask.SAVE_ERROR:
Toast.makeText(getApplicationContext(), getString(R.string.data_saved_error), Toast.LENGTH_LONG).show();
break;
}
}
/*
* Perform final initialization of the new field and assign it and it's related
* objects a place in the respective field, bind, instance (and itext) state arrays.
*/
private void addNewField(Field f)
{
// No longer a virgin
f.setNewField(false);
// Handle groups, repeated groups and other control field types separately
if (f.getType().equals("group")) {
if (Field.isRepeatedGroup(f)) {
addNewRepeatedGroupField(f);
} else {
addNewGroupField(f);
}
} else {
addNewRegularField(f);
}
}
private void addNewGroupField(Field f)
{
// Assign this group field as a child of the currently active field
Field parent = returnActiveField(null);
if (Field.isRepeatedGroup(parent)) {
f.setParent(parent.getRepeat());
parent.getRepeat().getChildren().add(f);
} else {
f.setParent(parent);
// If parent is null then we are at the top of the form
if (parent == null) {
Collect.getInstance().getFormBuilderState().getFields().add(f);
} else {
parent.getChildren().add(f);
}
}
}
private void addNewRepeatedGroupField(Field f)
{
/*
* Assign this repeated group field as a child of the currently active field (its parent)
* (taking into account the complexity of repeated groups)
*/
Field p = returnActiveField(null);
String xpath = determineXPathInheritance(p, f);
// Set xpath of repeat
f.getRepeat().setXPath(xpath);
// Set XPath of bind
f.getRepeat().getBind().setXPath(xpath);
// Set xpath of instance
f.getRepeat().getInstance().setXPath(xpath);
// Associate field with instance
f.getRepeat().getInstance().setField(f);
// Add the field either at the top level or as the child of a group
if (Field.isRepeatedGroup(p)) {
f.setParent(p.getRepeat());
p.getRepeat().getChildren().add(f);
} else {
f.setParent(p);
// If parent is null then we are at the top of the form
if (p == null) {
Collect.getInstance().getFormBuilderState().getFields().add(f);
} else {
p.getChildren().add(f);
}
}
// Also add the bind
Collect.getInstance().getFormBuilderState().getBinds().add(f.getRepeat().getBind());
attachFieldToInstanceParent(p, f.getRepeat());
}
private void addNewRegularField(Field f)
{
// Assign this field as a child of the currently active field (its parent)
Field p = returnActiveField(null);
String xpath = determineXPathInheritance(p, f);
// Use XPath for associated instance and bind as well as this field
f.getInstance().setXPath(xpath);
f.getBind().setXPath(xpath);
f.setXPath(xpath);
// Associate field with instance
f.getInstance().setField(f);
// Add the field either at the top level or as the child of a group
if (Field.isRepeatedGroup(p)) {
f.setParent(p.getRepeat());
p.getRepeat().getChildren().add(f);
} else {
f.setParent(p);
if (p == null) {
Collect.getInstance().getFormBuilderState().getFields().add(f);
} else {
p.getChildren().add(f);
}
}
// Binds are a flat list, so it does not matter where they are added
Collect.getInstance().getFormBuilderState().getBinds().add(f.getBind());
attachFieldToInstanceParent(p, f);
}
/*
* Associate the field with an instance parent. This will either be the nearest repeated group OR
* the root of the instance, which ever comes first.
*/
private void attachFieldToInstanceParent(Field p, Field f)
{
if (Field.isRepeatedGroup(p)) {
p.getRepeat().getInstance().getChildren().add(f.getInstance());
} else if (p == null) {
Collect.getInstance().getFormBuilderState().getInstance().add(f.getInstance());
} else {
attachFieldToInstanceParent(p.getParent(), f);
}
}
/*
* Prompt shown to the user before they leave the field list
* (discard changes & quit, save changes & quit, return to form field list)
*/
private void createQuitDialog()
{
String[] items = {
getString(R.string.keep_changes),
getString(R.string.do_not_save)
};
mAlertDialog = new AlertDialog.Builder(this)
.setIcon(R.drawable.ic_dialog_alert)
.setTitle(getString(R.string.tf_form_builder_exit))
.setNeutralButton(getString(R.string.do_not_exit), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id)
{
dialog.cancel();
}
})
.setItems(items,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which)
{
switch (which) {
case 0:
// Save and exit
mSaveFormDefinitionTask = new SaveFormDefinitionTask();
mSaveFormDefinitionTask.setFormSavedListener(FieldList.this);
mSaveFormDefinitionTask.execute(SaveToDiskTask.SAVED_AND_EXIT);
showDialog(SAVING_DIALOG);
break;
case 1:
// Discard any changes and exit
try {
if (mForm.getStatus() == FormDefinition.Status.placeholder)
Collect.getInstance().getDbService().getDb().delete(mForm);
} catch (Exception e) {
if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "unable to remove temporary document");
e.printStackTrace();
}
finish();
break;
}
}
}).create();
mAlertDialog.show();
}
/*
* Iterate through all fields at the same depth as the new one and
* ensure that the XPath is unique. Modify it with a counter if it
* is not.
*/
private String createUniqueXPath(final ArrayList<Field> fieldsAtSameDepth, String path)
{
Iterator<Field> it = fieldsAtSameDepth.iterator();
while (it.hasNext()) {
Field f = it.next();
// XPath of repeated groups is located at a different level
if ((Field.isRepeatedGroup(f) && f.getRepeat().hasXPath() && f.getRepeat().getXPath().equals(path)) ||
(f.hasXPath() && f.getXPath().equals(path))) {
// Capture counter from path
Pattern pattern = Pattern.compile(".*([0-9]+)$");
Matcher matcher = pattern.matcher(path);
Integer counter = 1;
if (matcher.find()) {
path = path.replaceFirst(matcher.group(1) + "$", "");
counter = Integer.valueOf(matcher.group(1));
counter++;
}
return createUniqueXPath(fieldsAtSameDepth, path + counter);
}
}
return path;
}
/*
* Determine XPath inheritance by traversing upwards in the tree from the current location.
*
* The XPath will be determined from the XPath of the closest repeated group or from the
* root of the instance, which ever comes first.
*/
private String determineXPathInheritance(Field parent, Field field)
{
String xpath = "";
if (parent == null) {
xpath = createUniqueXPath(Collect.getInstance().getFormBuilderState().getFields(), File.separator + mInstanceRoot + File.separator + Field.makeFieldName(field.getLabel()));
} else {
if (Field.isRepeatedGroup(parent)) {
xpath = createUniqueXPath(parent.getRepeat().getChildren(), parent.getRepeat().getXPath() + File.separator + Field.makeFieldName(field.getLabel()));
} else {
xpath = determineXPathInheritance(parent.getParent(), field);
}
}
return xpath;
}
/*
* Finds the current active field, sets it to inactive and either returns
* null to signal that the "top level" of the form has been reached or
* sets the parent field to active and returns it.
*/
private Field gotoParentField(Field f)
{
@SuppressWarnings("unused")
final String tt = t + "gotoParentField(): ";
Iterator<Field> it = null;
if (f == null)
it = mFieldState.iterator();
else {
if (f.isActive()) {
f.setActive(false);
if (f.getParent() == null) {
return null;
} else if (f.getParent().getType().equals("repeat")) {
// Set the parent of our parent (e.g., a group) active and return it
f.getParent().getParent().setActive(true);
return f.getParent().getParent();
} else {
f.getParent().setActive(true);
return f.getParent();
}
}
it = f.getChildren().iterator();
}
while (it.hasNext()) {
Field result = gotoParentField(it.next());
if (result instanceof Field)
return result;
}
return null;
}
private void goUpLevel()
{
Field destination;
// Special logic to hide the complexity of repeated elements
if (mActualPath.size() > mPath.size()) {
/*
* This will evaluate to true when we have navigated into a repeated group since
* the actual representation is <group><label>...</label><repeat ... /></group>
* and we want to represent it as one field vs. travelling two depths to get at
* the list of repeated elements.
*/
mPath.remove(mPath.size() - 1); // Remove the "group" label
mActualPath.remove(mActualPath.size() - 1); // Remove the repeated element
mActualPath.remove(mActualPath.size() - 1); // Remove the "group" element
} else {
mPath.remove(mPath.size() - 1);
mActualPath.remove(mActualPath.size() - 1); // Remove the group element
}
destination = gotoParentField(null);
if (destination == null)
refreshView(mFieldState);
else {
// Special support for nested repeated groups
if (Field.isRepeatedGroup(destination)) {
mActualPath.add(destination.getLabel().toString());
mActualPath.add(destination.getRepeat().getLabel().toString());
refreshView(destination.getRepeat().getChildren());
} else {
refreshView(destination.getChildren());
}
}
}
/*
* Refresh the view (displaying the currently active field
* or the top level of the form if no field is currently active)
*/
private void refreshView()
{
// Don't attempt to refresh the view if loading or saving
if (mProgressDialog != null && mProgressDialog.isShowing()) {
return;
}
Field destination = returnActiveField(null);
if (destination == null)
refreshView(mFieldState);
else {
// Special support for nested repeated groups
if (Field.isRepeatedGroup(destination))
refreshView(destination.getRepeat().getChildren());
else
refreshView(destination.getChildren());
}
}
/*
* Refresh the view and display whatever fields are passed here
*/
private void refreshView(ArrayList<Field> fieldsToDisplay)
{
setTitle(getString(R.string.app_name) + " > " + getString(R.string.tf_editing) + " " + mForm.getName());
String pathText = "";
if (mPath.isEmpty()) {
pathText = getString(R.string.tf_at_top_of_form);
jumpPreviousButton.setEnabled(false);
} else {
Iterator<String> it = mPath.iterator();
while (it.hasNext()) {
String d = it.next();
if (pathText.length() > 0)
pathText = pathText + " > " + d;
else
pathText = "Top > " + d;
}
jumpPreviousButton.setEnabled(true);
}
mPathText.setText(pathText);
mAdapter = new FormBuilderFieldListAdapter(getApplicationContext(), fieldsToDisplay);
// Provide a hint to users if the list is empty
if (mAdapter.isEmpty()) {
TextView nothingToDisplay = (TextView) findViewById(R.id.nothingToDisplay);
nothingToDisplay.setVisibility(View.VISIBLE);
} else {
TextView nothingToDisplay = (TextView) findViewById(R.id.nothingToDisplay);
nothingToDisplay.setVisibility(View.INVISIBLE);
}
setListAdapter(mAdapter);
TouchListView tlv = (TouchListView) getListView();
tlv.setDropListener(onDrop);
tlv.setRemoveListener(onRemove);
}
/*
* Finds the currently active fields and returns it (or null to indicate no active field)
*
* Does not change the state of the currently active field, as opposed to gotoParentField()
*/
private Field returnActiveField(Field f)
{
@SuppressWarnings("unused")
final String tt = t + "returnActiveField(): ";
Iterator<Field> it = null;
if (f == null) {
it = mFieldState.iterator();
} else {
if (f.isActive())
return f;
it = f.getChildren().iterator();
}
while (it.hasNext()) {
Field result = returnActiveField(it.next());
if (result instanceof Field)
return result;
}
return null;
}
/*
* Launch the element editor either to add a new field or to modify an existing one
*/
private void startFieldEditor(String type, Field field)
{
Collect.getInstance().getFormBuilderState().setField(field);
Intent i = new Intent(this, FieldEditorActivity.class);
i.putExtra(FieldEditorActivity.KEY_FIELDTYPE, type);
startActivityForResult(i, REQUEST_EDITFIELD);
}
}