Java tutorial
/* * Copyright (C) 2012 University of Washington * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package org.opendatakit.tables.utils; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import java.util.UUID; import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringEscapeUtils; import org.kxml2.io.KXmlParser; import org.kxml2.kdom.Document; import org.kxml2.kdom.Element; import org.kxml2.kdom.Node; import org.opendatakit.aggregate.odktables.rest.ElementDataType; import org.opendatakit.aggregate.odktables.rest.ElementType; import org.opendatakit.aggregate.odktables.rest.SavepointTypeManipulator; import org.opendatakit.aggregate.odktables.rest.TableConstants; import org.opendatakit.common.android.data.ColumnDefinition; import org.opendatakit.common.android.database.DatabaseFactory; import org.opendatakit.common.android.provider.DataTableColumns; import org.opendatakit.common.android.utilities.ColumnUtil; import org.opendatakit.common.android.utilities.DataUtil; import org.opendatakit.common.android.utilities.GeoColumnUtil; import org.opendatakit.common.android.utilities.KeyValueHelper; import org.opendatakit.common.android.utilities.KeyValueStoreHelper; import org.opendatakit.common.android.utilities.ODKDatabaseUtils; import org.opendatakit.common.android.utilities.ODKFileUtils; import org.opendatakit.common.android.utilities.RowPathColumnUtil; import org.opendatakit.common.android.utilities.TableUtil; import org.opendatakit.common.android.utilities.WebLogger; import org.opendatakit.common.android.utilities.WebUtils; import org.xmlpull.v1.XmlPullParserException; import android.app.Activity; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.provider.BaseColumns; /** * Utility methods for using ODK Collect. * * @author:sudar.sam@gmail.com --and somebody else, unknown */ public class CollectUtil { private static final String COLLECT_KEY_LAST_STATUS_CHANGE_DATE = "date"; private static final String TAG = "CollectUtil"; public static final String KVS_PARTITION = "CollectUtil"; public static final String KVS_ASPECT = "default"; /** * This is the name of the shared preference to which collect util will save * the things it must retain. At the moment this is only the row id that is * being edited. The vast majority of state should not be saved here. */ private static final String SHARED_PREFERENCE_NAME = "CollectUtil_Preference"; /** * This is the key name of the preference whose value will be the row id that * is currently being edited. */ private static final String PREFERENCE_KEY_EDITED_ROW_ID = "editedRowId"; /** * This is the table id of the tableId that will be receiving an add row. This * is necessary because javascript views can launch adds for tables other than * themselves, and this preference will store which table id the row should be * added to. */ private static final String PREFERENCE_KEY_TABLE_ID_ADD = "tableIdAdd"; /* * The names here should match those in the version of collect that is on the * phone. They came from InstanceProviderApi. */ public static final String COLLECT_KEY_STATUS = "status"; public static final String COLLECT_KEY_STATUS_INCOMPLETE = "incomplete"; public static final String COLLECT_KEY_STATUS_COMPLETE = "complete"; public static final String COLLECT_KEY_CAN_EDIT_WHEN_COMPLETE = "canEditWhenComplete"; public static final String COLLECT_KEY_SUBMISSION_URI = "submissionUri"; public static final String COLLECT_KEY_INSTANCE_FILE_PATH = "instanceFilePath"; public static final String COLLECT_KEY_JR_FORM_ID = "jrFormId"; public static final String COLLECT_KEY_JR_VERSION = "jrVersion"; public static final String COLLECT_KEY_DISPLAY_NAME = "displayName"; public static final String COLLECT_INSTANCE_ORDER_BY = BaseColumns._ID + " asc"; public static final String COLLECT_INSTANCE_AUTHORITY = "org.odk.collect.android.provider.odk.instances"; public static final Uri CONTENT_INSTANCE_URI = Uri .parse("content://" + COLLECT_INSTANCE_AUTHORITY + "/instances"); public static final String COLLECT_FORM_AUTHORITY = "org.odk.collect.android.provider.odk.forms"; public static final Uri CONTENT_FORM_URI = Uri.parse("content://" + COLLECT_FORM_AUTHORITY + "/forms"); public static final String COLLECT_KEY_FORM_FILE_PATH = "formFilePath"; private static final String COLLECT_FORMS_URI_STRING = "content://org.odk.collect.android.provider.odk.forms/forms"; @SuppressWarnings("unused") private static final Uri ODKCOLLECT_FORMS_CONTENT_URI = Uri.parse(COLLECT_FORMS_URI_STRING); private static final String COLLECT_INSTANCES_URI_STRING = "content://org.odk.collect.android.provider.odk.instances/instances"; private static final Uri COLLECT_INSTANCES_CONTENT_URI = Uri.parse(COLLECT_INSTANCES_URI_STRING); /******************** * Keys present in the Key Value Store. These should represent data about the * form that is present if a form is defined for a particular table. ********************/ public static final String KEY_FORM_VERSION = "CollectUtil.collectFormVersion"; public static final String KEY_FORM_ID = "CollectUtil.formId"; public static final String KEY_FORM_ROOT_ELEMENT = "CollectUtil.rootElement"; /** * The default value of the root element for the form. */ public static final String DEFAULT_ROOT_ELEMENT = "data"; private static final String COLLECT_ADDROW_FORM_ID_PREFIX = "tablesId_"; /** * Return the formId for the single file that will be written when there is no * custom form defined for a table. * * @param tp * @return */ private static String getDefaultAddRowFormId(String tableId) { return COLLECT_ADDROW_FORM_ID_PREFIX + tableId; } /** * This is the file name and path of the single file that will be written that * contains the key values of column name to data for a given row that was * created. * * @return */ private static File getAddRowFormFile(String appName, String tableId) { return new File(ODKFileUtils.getTablesFolder(appName, tableId), "addrowform.xml"); } /** * This is the file name and path of the single file that will be written that * contains the key values of column name to data for a given row that will be * edited. * * @return */ private static File getEditRowFormFile(String appName, String tableId, String rowId) { return new File(ODKFileUtils.getInstanceFolder(appName, tableId, rowId), "editRowData.xml"); } /** * Build a default form. This form will allow being swiped through, one field * at a time. * * @param file * the file to write the form to * @param columns * the columnProperties of the table. * @param title * the title of the form * @param formId * the id of the form * @return true if the file was successfully written */ private static boolean buildBlankForm(Context context, String appName, String tableId, ArrayList<ColumnDefinition> orderedDefns, File file, String formId) { OutputStreamWriter writer = null; try { List<ColumnDefinition> geopointList = GeoColumnUtil.get().getGeopointColumnDefinitions(orderedDefns); List<ColumnDefinition> uriList = RowPathColumnUtil.get().getUriColumnDefinitions(orderedDefns); ArrayList<ColumnDefinition> orderedElements = orderedDefns; String localizedDisplayName; SQLiteDatabase db = null; try { db = DatabaseFactory.get().getDatabase(context, appName); localizedDisplayName = TableUtil.get().getLocalizedDisplayName(db, tableId); } finally { if (db != null) { db.close(); } } FileOutputStream out = new FileOutputStream(file); writer = new OutputStreamWriter(out, CharEncoding.UTF_8); writer.write( "<h:html xmlns=\"http://www.w3.org/2002/xforms\" " + "xmlns:h=\"http://www.w3.org/1999/xhtml\" " + "xmlns:ev=\"http://www.w3.org/2001/xml-events\" " + "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" " + "xmlns:jr=\"http://openrosa.org/javarosa\">"); writer.write("<h:head>"); writer.write("<h:title>"); writer.write(StringEscapeUtils.escapeXml(localizedDisplayName)); writer.write("</h:title>"); writer.write("<model>"); writer.write("<instance>"); writer.write("<"); writer.write(DEFAULT_ROOT_ELEMENT); writer.write(" "); writer.write("id=\""); writer.write(StringEscapeUtils.escapeXml(formId)); writer.write("\">"); for (ColumnDefinition cd : orderedElements) { ColumnDefinition cdContainingElement = cd.getParent(); if (cdContainingElement != null) { if (geopointList.contains(cdContainingElement) || uriList.contains(cdContainingElement)) { // processed by the containing type continue; } // and if this is not a unit of retention, a containing element is // handling it. if (!cd.isUnitOfRetention()) { continue; } } // ok. we are directly processing this... and possibly sucking values // out of sub-elements... writer.write("<"); writer.write(cd.getElementKey()); writer.write("/>"); } writer.write("<meta><instanceID/></meta>"); writer.write("</"); writer.write(DEFAULT_ROOT_ELEMENT); writer.write(">"); writer.write("</instance>"); ElementTypeManipulator m = ElementTypeManipulatorFactory.getInstance(appName); for (ColumnDefinition cd : orderedElements) { ColumnDefinition cdContainingElement = cd.getParent(); if (cdContainingElement != null) { if (geopointList.contains(cdContainingElement) || uriList.contains(cdContainingElement)) { // processed by the containing type continue; } // and if this is not a unit of retention, a containing element is // handling it. if (!cd.isUnitOfRetention()) { continue; } } // ok. we are directly processing this... and possibly sucking values // out of sub-elements... ElementType type = cd.getType(); String collectType = m.getDefaultRenderer(cd.getType()).getCollectType(); if (collectType == null) { collectType = "string"; } writer.write("<bind nodeset=\"/"); writer.write(DEFAULT_ROOT_ELEMENT); writer.write("/"); writer.write(cd.getElementKey()); writer.write("\" type=\""); writer.write(collectType); writer.write("\"/>"); } writer.write("<bind nodeset=\"/"); writer.write(DEFAULT_ROOT_ELEMENT); writer.write("/meta/instanceID\" type=\"string\" required=\"true()\"/>"); writer.write("<itext>"); writer.write("<translation lang=\"eng\">"); for (ColumnDefinition cd : orderedElements) { ColumnDefinition cdContainingElement = cd.getParent(); if (cdContainingElement != null) { if (geopointList.contains(cdContainingElement) || uriList.contains(cdContainingElement)) { // processed by the containing type continue; } // and if this is not a unit of retention, a containing element is // handling it. if (!cd.isUnitOfRetention()) { continue; } } db = null; try { db = DatabaseFactory.get().getDatabase(context, appName); localizedDisplayName = ColumnUtil.get().getLocalizedDisplayName(db, tableId, cd.getElementKey()); } finally { if (db != null) { db.close(); } } // ok. we are directly processing this... and possibly sucking values // out of sub-elements... writer.write("<text id=\"/"); writer.write(DEFAULT_ROOT_ELEMENT); writer.write("/"); writer.write(cd.getElementKey()); writer.write(":label\">"); writer.write("<value>"); writer.write(localizedDisplayName); writer.write("</value>"); writer.write("</text>"); } writer.write("</translation>"); writer.write("</itext>"); writer.write("</model>"); writer.write("</h:head>"); writer.write("<h:body>"); for (ColumnDefinition cd : orderedElements) { ColumnDefinition cdContainingElement = cd.getParent(); if (cdContainingElement != null) { if (geopointList.contains(cdContainingElement) || uriList.contains(cdContainingElement)) { // processed by the containing type continue; } // and if this is not a unit of retention, a containing element is // handling it. if (!cd.isUnitOfRetention()) { continue; } } // ok. we are directly processing this... and possibly sucking values // out of sub-elements... String action = "input"; String additionalAttributes = ""; if (uriList.contains(cd)) { action = "upload"; String basetype = cd.getElementName().substring(0, cd.getElementName().length() - 3); if (basetype.equals("mime")) { basetype = "*"; // not supported by ODK Collect... launch OI File // Manager? } additionalAttributes = " mediatype=\"" + basetype + "/*\""; } writer.write("<" + action + additionalAttributes + " ref=\"/" + DEFAULT_ROOT_ELEMENT + "/" + cd.getElementKey() + "\">"); writer.write("<label ref=\"jr:itext('/" + DEFAULT_ROOT_ELEMENT + "/" + cd.getElementKey() + ":label')\"/>"); writer.write("</" + action + ">"); } writer.write("</h:body>"); writer.write("</h:html>"); writer.flush(); writer.close(); return true; } catch (IOException e) { // TODO Auto-generated catch block WebLogger.getLogger(appName).printStackTrace(e); return false; } finally { try { writer.close(); } catch (IOException e) { } } } private static String getJrFormId(String appName, String filepath) { Document formDoc = parseForm(appName, filepath); String namespace = formDoc.getRootElement().getNamespace(); Element hhtmlEl = formDoc.getElement(namespace, "h:html"); Element hheadEl = hhtmlEl.getElement(namespace, "h:head"); Element modelEl = hheadEl.getElement(namespace, "model"); Element instanceEl = modelEl.getElement(namespace, "instance"); Element dataEl = instanceEl.getElement(1); return dataEl.getAttributeValue(namespace, "id"); } private static Document parseForm(String appName, String filepath) { File xmlFile = new File(filepath); InputStream is; try { is = new FileInputStream(xmlFile); } catch (FileNotFoundException e) { throw new IllegalStateException(e); } // Now get the reader. InputStreamReader isr = null; try { isr = new InputStreamReader(is, Charset.forName(CharEncoding.UTF_8)); } catch (UnsupportedCharsetException e) { WebLogger.getLogger(appName).w(TAG, "UTF-8 wasn't supported--trying with default charset"); isr = new InputStreamReader(is); } Document formDoc = new Document(); KXmlParser formParser = new KXmlParser(); try { formParser.setInput(isr); formDoc.parse(formParser); } catch (FileNotFoundException e) { // TODO Auto-generated catch block WebLogger.getLogger(appName).printStackTrace(e); } catch (XmlPullParserException e) { // TODO Auto-generated catch block WebLogger.getLogger(appName).printStackTrace(e); } catch (IOException e) { // TODO Auto-generated catch block WebLogger.getLogger(appName).printStackTrace(e); } finally { try { isr.close(); } catch (IOException e) { } } return formDoc; } /** * Write the row for a table out to a file to later be inserted into an * existing Collect form. This existing form must match the fields specified * in params. * <p> * The file generated is at the location and name specified in * {@link DATA_FILE_PATH_AND_NAME}. * * TODO: add support for select-multiple * * The mechanics of this are modeled on the getIntentForOdkCollectEditRow * method in Controller that handles the case for editing every column in a * screen by screen fashion, generating the entire form on the fly. * * @param context * @param appName * @param tableId * @param orderedDefns * @param values * @param params * the form parameters * @param rowId * @return true if the write succeeded */ private static boolean writeRowDataToBeEdited(Context context, String appName, String tableId, ArrayList<ColumnDefinition> orderedDefns, Map<String, String> values, CollectFormParameters params, String rowId) { /* * This is currently implemented thinking that all you need to have is: * * <?xml version='1.0' ?><data id="tablesaddrowformid"> * * followed by a series of: * * <columnName1>firstFieldData</columnName1> ... * <lastColumn>lastField</lastField> * * We will just go ahead and write all the fields/columns, knowing that the * form will simply ignore those for which it does not have matching entry * fields. */ List<ColumnDefinition> geopointList = GeoColumnUtil.get().getGeopointColumnDefinitions(orderedDefns); List<ColumnDefinition> uriList = RowPathColumnUtil.get().getUriColumnDefinitions(orderedDefns); OutputStreamWriter writer = null; try { FileOutputStream out = new FileOutputStream(getEditRowFormFile(appName, tableId, rowId)); writer = new OutputStreamWriter(out, CharEncoding.UTF_8); writer.write("<?xml version='1.0' ?><"); writer.write(params.getRootElement()); writer.write(" id=\""); writer.write(StringEscapeUtils.escapeXml(params.getFormId())); writer.write("\">"); for (ColumnDefinition cd : orderedDefns) { ColumnDefinition cdContainingElement = cd.getParent(); if (cdContainingElement != null) { if (geopointList.contains(cdContainingElement) || uriList.contains(cdContainingElement)) { // processed by the containing type continue; } // and if this is not a unit of retention, a containing element is // handling it. if (!cd.isUnitOfRetention()) { continue; } } // ok. we are directly processing this... and possibly sucking values // out of sub-elements... ElementType type = cd.getType(); if (geopointList.contains(cd)) { // find its children... List<ColumnDefinition> children = cd.getChildren(); ColumnDefinition[] cparray = new ColumnDefinition[4]; if (!children.isEmpty()) { cparray[0] = children.get(0); } if (children.size() > 1) { cparray[1] = children.get(1); } if (children.size() > 2) { cparray[2] = children.get(2); } if (children.size() > 3) { cparray[3] = children.get(3); } ColumnDefinition cplat = null, cplng = null, cpalt = null, cpacc = null; for (ColumnDefinition scp : cparray) { if (scp.getElementName().equals("latitude")) { cplat = scp; } else if (scp.getElementName().equals("longitude")) { cplng = scp; } else if (scp.getElementName().equals("altitude")) { cpalt = scp; } else if (scp.getElementName().equals("accuracy")) { cpacc = scp; } } boolean nonNull = false; StringBuilder b = new StringBuilder(); if (cplat != null) { String value = (values == null) ? null : values.get(cplat.getElementKey()); if (value == null) { b.append("-999999"); } else { nonNull = true; b.append(value); } } b.append(" "); if (cplng != null) { String value = (values == null) ? null : values.get(cplng.getElementKey()); if (value == null) { b.append("-999999"); } else { nonNull = true; b.append(value); } } b.append(" "); if (cpalt != null) { String value = (values == null) ? null : values.get(cpalt.getElementKey()); if (value == null) { b.append("-999999"); } else { nonNull = true; b.append(value); } } b.append(" "); if (cpacc != null) { String value = (values == null) ? null : values.get(cpacc.getElementKey()); if (value == null) { b.append("-999999"); } else { nonNull = true; b.append(value); } } if (nonNull) { writer.write("<"); writer.write(cd.getElementKey()); writer.write(">"); writer.write(StringEscapeUtils.escapeXml(b.toString())); writer.write("</"); writer.write(cd.getElementKey()); writer.write(">"); } else { writer.write("<"); writer.write(cd.getElementKey()); writer.write("/>"); } } else if (uriList.contains(cd)) { // find its children... List<ColumnDefinition> children = cd.getChildren(); ColumnDefinition[] cparray = new ColumnDefinition[children.size()]; for (int i = 0; i < children.size(); ++i) { cparray[i] = children.get(i); } // find the uriFragment ColumnDefinition cpfrag = null; for (ColumnDefinition scp : cparray) { if (scp.getElementName().equals("uriFragment")) { cpfrag = scp; } } String value = null; if (cpfrag != null) { value = (values == null) ? null : values.get(cpfrag.getElementKey()); } if (value != null) { File f = ODKFileUtils.getAsFile(appName, value); value = f.getName(); } if (value != null) { writer.write("<"); writer.write(cd.getElementKey()); writer.write(">"); writer.write(StringEscapeUtils.escapeXml(value)); writer.write("</"); writer.write(cd.getElementKey()); writer.write(">"); } else { writer.write("<"); writer.write(cd.getElementKey()); writer.write("/>"); } } else if (cd.isUnitOfRetention()) { String value = (values == null) ? null : values.get(cd.getElementKey()); if (value != null) { writer.write("<"); writer.write(cd.getElementKey()); writer.write(">"); if (type.getElementType().equals(ElementType.DATE)) { // TODO: get this in the correct format... writer.write(StringEscapeUtils.escapeXml(value)); } else if (type.getElementType().equals(ElementType.DATETIME)) { // TODO: get this in the correct format... writer.write(StringEscapeUtils.escapeXml(value)); } else if (type.getElementType().equals(ElementType.TIME)) { // TODO: get this in the correct format... writer.write(StringEscapeUtils.escapeXml(value)); } else { writer.write(StringEscapeUtils.escapeXml(value)); } writer.write("</"); writer.write(cd.getElementKey()); writer.write(">"); } else { writer.write("<"); writer.write(cd.getElementKey()); writer.write("/>"); } } } writer.write("<meta>"); writer.write("<instanceID>"); writer.write(StringEscapeUtils.escapeXml(rowId)); writer.write("</instanceID>"); writer.write("</meta>"); writer.write("</"); writer.write(params.getRootElement()); writer.write(">"); writer.flush(); writer.close(); return true; } catch (IOException e) { WebLogger.getLogger(appName).e(TAG, "IOException while writing data file"); WebLogger.getLogger(appName).printStackTrace(e); return false; } finally { try { writer.close(); } catch (IOException e) { } } } private static boolean isExistingCollectInstanceForRowData(Context context, String appName, String tableId, String rowId) { Cursor c = null; try { String instanceFilePath = getEditRowFormFile(appName, tableId, rowId).getAbsolutePath(); c = context.getContentResolver().query(CONTENT_INSTANCE_URI, null, COLLECT_KEY_INSTANCE_FILE_PATH + "=?", new String[] { instanceFilePath }, COLLECT_INSTANCE_ORDER_BY); if (c.getCount() == 0) { c.close(); return false; } c.close(); return true; } catch (Exception e) { WebLogger.getLogger(appName).w(TAG, "caught an exception while deleting an instance, " + "ignoring and proceeding"); return true; // since we don't really know what is going on... } finally { if (c != null && !c.isClosed()) { c.close(); } } } /** * Insert the values existing in the file specified by * {@link DATA_FILE_PATH_AND_NAME} into the form specified by params. * <p> * If the display name is not defined in the {@code params} parameter then the * string resource is used. * <p> * The inserted row is marked as INCOMPLETE. * <p> * PRECONDITION: in order to be populated with data, the data file containing * the row's data must have been written, most likely by calling * writeRowDataToBeEdited(). * <p> * PRECONDITION: previous instances should already have been deleted by now, * or the passed in file names should be uniqued by adding timestamps, or * something. * * @param params * the identifying parameters for the form. Should be the same object * used to write the instance file. * @param rowNum * the row number of the row being edited * @param resolver * the ContentResolver of the activity making the request. * @return */ /* * This is based on the code at: http://code.google.com/p/opendatakit/source/ * browse/src/org/odk/collect/android/tasks/SaveToDiskTask.java?repo=collect * in the method updateInstanceDatabase(). */ private static Uri getUriForCollectInstanceForRowData(Context context, String appName, String tableId, CollectFormParameters params, String rowId, boolean shouldUpdate) { String instanceFilePath = getEditRowFormFile(appName, tableId, rowId).getAbsolutePath(); ContentValues values = new ContentValues(); // First we need to fill the values with various little things. values.put(COLLECT_KEY_STATUS, COLLECT_KEY_STATUS_INCOMPLETE); values.put(COLLECT_KEY_CAN_EDIT_WHEN_COMPLETE, Boolean.toString(true)); values.put(COLLECT_KEY_INSTANCE_FILE_PATH, instanceFilePath); values.put(COLLECT_KEY_JR_FORM_ID, params.getFormId()); values.put(COLLECT_KEY_DISPLAY_NAME, params.getRowDisplayName() + "_" + WebUtils.get().iso8601Date(new Date())); // only add the version if it exists (ie not null) if (params.getFormVersion() != null) { values.put(COLLECT_KEY_JR_VERSION, params.getFormVersion()); } ContentResolver resolver = context.getContentResolver(); Uri uriOfForm; if (shouldUpdate) { int count = resolver.update(CONTENT_INSTANCE_URI, values, COLLECT_KEY_INSTANCE_FILE_PATH + "=?", new String[] { instanceFilePath }); if (count == 0) { uriOfForm = resolver.insert(CONTENT_INSTANCE_URI, values); } else { Cursor c = null; try { c = resolver.query(CONTENT_INSTANCE_URI, null, COLLECT_KEY_INSTANCE_FILE_PATH + "=?", new String[] { instanceFilePath }, COLLECT_INSTANCE_ORDER_BY); if (c.moveToFirst()) { // we got a result, meaning that the form exists in collect. // so we just need to set the URI. int collectInstanceKey; // this is the primary key of the form in // Collect's // database. collectInstanceKey = ODKDatabaseUtils.get().getIndexAsType(c, Integer.class, c.getColumnIndexOrThrow(BaseColumns._ID)); uriOfForm = (Uri.parse(CONTENT_INSTANCE_URI + "/" + collectInstanceKey)); c.close(); } else { c.close(); throw new IllegalStateException("it was updated we should have found the record!"); } } finally { if (c != null && !c.isClosed()) { c.close(); } } } } else { // now we want to get the uri for the insertion. uriOfForm = resolver.insert(CONTENT_INSTANCE_URI, values); } return uriOfForm; } /** * Delete the form specified by the id given in the parameters. Does not check * form version. * * @param resolver * ContentResolver of the calling activity * @param formId * the id of the form to be deleted * @return the result of the the delete call */ private static int deleteForm(ContentResolver resolver, String appName, String formId) { try { return resolver.delete(CONTENT_FORM_URI, COLLECT_KEY_JR_FORM_ID + "=?", new String[] { formId }); } catch (Exception e) { WebLogger.getLogger(appName).d(TAG, "caught an exception while deleting a form, returning 0 and " + "proceeding"); return 0; } } /** * Insert a form into collect. Returns the URI of the inserted form. Note that * form version is not passed in with the content values and is likely * therefore not considered. (Not sure exactly how collect checks this.) * <p> * Precondition: the form should not exist in Collect before this call is * made. In other words, the a query made on the form should return no * results. * * @param resolver * the ContentResolver of the calling activity * @param formFilePath * the filePath to the form * @param displayName * the displayName of the form * @param formId * the id of the form * @return the result of the insert call, likely the URI of the resulting * form. If the form was not first deleted there could be a problem */ private static Uri insertFormIntoCollect(ContentResolver resolver, String formFilePath, String displayName, String formId) { ContentValues insertValues = new ContentValues(); insertValues.put(COLLECT_KEY_FORM_FILE_PATH, formFilePath); insertValues.put(COLLECT_KEY_DISPLAY_NAME, displayName); insertValues.put(COLLECT_KEY_JR_FORM_ID, formId); return resolver.insert(CONTENT_FORM_URI, insertValues); } /** * Return the URI of the form for adding a row to a table. If the formId is * custom defined it must exist to Collect (most likely by putting the form in * Collect's form folder and starting Collect once). If the form does not * exist, it inserts the static addRowForm information into Collect. * <p> * Display name only matters if it is a programmatically generated form. * <p> * Precondition: If formId refers to a custom form, it must have already been * scanned in and known to exist to Collect. If the formId is not custom, but * refers to a form built on the fly, it should be the id of * {@link COLLECT_ADDROW_FORM_ID}, and the form should already have been * written. * * @param resolver * ContentResolver of the calling activity * @param appName * application name. * @param formId * id of the form whose uri will be returned * @param formDisplayName * display name of the table. Only pertinent if the form has been * programmatically generated. * @return the uri of the form. */ private static Uri getUriOfForm(ContentResolver resolver, String appName, String formId) { Uri resultUri = null; Cursor c = null; try { c = resolver.query(CollectUtil.CONTENT_FORM_URI, null, CollectUtil.COLLECT_KEY_JR_FORM_ID + "=?", new String[] { formId }, null); if (!c.moveToFirst()) { WebLogger.getLogger(appName).e(TAG, "query of Collect for form returned no results"); } else { // we got a result, meaning that the form exists in collect. // so we just need to set the URI. int collectFormKey; // this is the primary key of the form in // Collect's // database. collectFormKey = ODKDatabaseUtils.get().getIndexAsType(c, Integer.class, c.getColumnIndexOrThrow(BaseColumns._ID)); resultUri = (Uri.parse(CollectUtil.CONTENT_FORM_URI + "/" + collectFormKey)); } } finally { if (c != null && !c.isClosed()) { c.close(); } } return resultUri; } /** * This is a convenience method that should be called when generating non-user * defined forms for adding or editing rows. It calls, in this order, * {@link deleteForm}, {@link buildBlankForm}, and * {@link insertFormIntoCollect}. * * @param resolver * content resolver of the calling activity * @param params * @param tp * @return true if every method returned successfully */ private static boolean deleteWriteAndInsertFormIntoCollect(Context context, String appName, String tableId, ArrayList<ColumnDefinition> orderedDefns, CollectFormParameters params) { if (params.isCustom()) { WebLogger.getLogger(appName).e(TAG, "passed custom form to be deleted, rewritten, and " + "inserted into Collect. Not performing task."); return false; } ContentResolver resolver = context.getContentResolver(); CollectUtil.deleteForm(resolver, appName, params.getFormId()); // First we want to write the file. boolean writeSuccessful = CollectUtil.buildBlankForm(context, appName, tableId, orderedDefns, getAddRowFormFile(appName, tableId), params.getFormId()); if (!writeSuccessful) { WebLogger.getLogger(appName).e(TAG, "problem writing file for add row"); return false; } String localizedDisplayName; SQLiteDatabase db = null; try { db = DatabaseFactory.get().getDatabase(context, appName); localizedDisplayName = TableUtil.get().getLocalizedDisplayName(db, tableId); } finally { if (db != null) { db.close(); } } // Now we want to insert the file. Uri insertedFormUri = CollectUtil.insertFormIntoCollect(resolver, getAddRowFormFile(appName, tableId).getAbsolutePath(), localizedDisplayName, params.getFormId()); if (insertedFormUri == null) { WebLogger.getLogger(appName).e(TAG, "problem inserting form into collect, return uri was null"); return false; } return true; } /** * Convenience method for calling * {@link #getIntentForOdkCollectAddRow(Context, String, String, ArrayList, CollectFormParameters, Map)} * followed by {@link #launchCollectToAddRow(Activity, Intent, String)}. * * @param activity * @param appName * @param tableId * @param orderedDefns * @param collectFormParameters * @param prepopulatedValues */ public static void addRowWithCollect(Activity activity, String appName, String tableId, ArrayList<ColumnDefinition> orderedDefns, CollectFormParameters collectFormParameters, Map<String, String> prepopulatedValues) { Intent addRowIntent = getIntentForOdkCollectAddRow(activity, appName, tableId, orderedDefns, collectFormParameters, prepopulatedValues); if (addRowIntent == null) { WebLogger.getLogger(appName).e(TAG, "[addRowWithCollect] intent was null, returning"); return; } launchCollectToAddRow(activity, addRowIntent, tableId); } /** * Launch Collect to edit a row. Convenience method for calling * {@link #getIntentForOdkCollectEditRow(Context, String, String, ArrayList, Map, String, String, String, String) * followed by {@link #launchCollectToEditRow(Activity, Intent, String)}. * * @param activity * @param appName * @param tableId * @param orderedDefns * @param rowId * @param collectFormParameters */ public static void editRowWithCollect(Activity activity, String appName, String tableId, ArrayList<ColumnDefinition> orderedDefns, String rowId, CollectFormParameters collectFormParameters) { Map<String, String> elementKeyToValue = WebViewUtil.getMapOfElementKeyToValue(activity, appName, tableId, orderedDefns, rowId); Intent editRowIntent = getIntentForOdkCollectEditRow(activity, appName, tableId, orderedDefns, elementKeyToValue, collectFormParameters.getFormId(), collectFormParameters.getFormVersion(), collectFormParameters.getRootElement(), rowId); if (editRowIntent == null) { WebLogger.getLogger(appName).e(TAG, "[editRowWithCollect] intent was null, doing nothing"); } else { launchCollectToEditRow(activity, editRowIntent, rowId); } } /** * This is a move away from the general "odk add row" usage that is going on * when no row is defined. As I understand it, the new case will work as * follows. * * There exits an "tableEditRow" form for a particular table. This form, as I * understand it, must exist both in the tables directory, as well as in * Collect so that Collect can launch it with an Intent. * * You then also construct a "values" sort of file, that is the data from the * database that will pre-populate the fields. Mitch referred to something * like this as the "instance" file. * * Once you have both of these files, the form and the data, you insert the * data into the form. When you launch the form, it is then pre-populated with * data from the database. * * In order to make this work, the form must exist both within the places * Collect knows to look, as well as in the Tables folder. You also must know * the: * * collectFormVersion collectFormId collectXFormRootElement (default to * "data") * * These will most likely exist as keys in the key value store. They must * match the form. * * Other things needed will be: * * instanceFilePath // I think the filepath with all the values displayName // * just text, eg a row ID formId // the same thing as collectFormId? * formVersion status // either INCOMPLETE or COMPLETE * * Examples for how this is done in Collect can be found in the Collect code * in org.odk.collect.android.tasks.SaveToDiskTask.java, in the * updateInstanceDatabase() method. * * The functionality to construct the elementKeyToValue array from the rowNum * and table has been elevated. Now only the * CollectUtil.getIntentForOdkCollectEditRow(...) method is exposed. * * @param context * @param appName * @param tableId * @param orderedDefns * @param elementKeyToValue * @param formId * @param formVersion * @param formRootElement * @param rowId * @return */ public static Intent getIntentForOdkCollectEditRow(Context context, String appName, String tableId, ArrayList<ColumnDefinition> orderedDefns, Map<String, String> elementKeyToValue, String formId, String formVersion, String formRootElement, String rowId) { CollectFormParameters formParameters = CollectFormParameters.constructCollectFormParameters(context, appName, tableId); if (formId != null && !formId.equals("")) { formParameters.setFormId(formId); } if (formVersion != null && !formVersion.equals("")) { formParameters.setFormVersion(formVersion); } if (formRootElement != null && !formRootElement.equals("")) { formParameters.setRootElement(formRootElement); } Intent editRowIntent = CollectUtil.getIntentForOdkCollectEditRow(context, appName, tableId, orderedDefns, elementKeyToValue, formParameters, rowId); return editRowIntent; } /** * Return an intent that can be used to edit a row. * <p> * The idea here is that we might want to edit a row of the table using a * pre-set Collect form. This form would be user-defined and would be a more * user-friendly thing that would display only the pertinent information for a * particular user. * * @param context * @param tp * @param elementKeyToValue * @param params * @return */ private static Intent getIntentForOdkCollectEditRow(Context context, String appName, String tableId, ArrayList<ColumnDefinition> orderedDefns, Map<String, String> elementKeyToValue, CollectFormParameters params, String rowId) { // Check if there is a custom form. If there is not, we want to delete // the old form and write the new form. if (!params.isCustom()) { boolean formIsReady = CollectUtil.deleteWriteAndInsertFormIntoCollect(context, appName, tableId, orderedDefns, params); if (!formIsReady) { WebLogger.getLogger(appName).e(TAG, "could not delete, write, or insert a generated form"); return null; } } boolean shouldUpdate = CollectUtil.isExistingCollectInstanceForRowData(context, appName, tableId, rowId); boolean writeDataSuccessful = CollectUtil.writeRowDataToBeEdited(context, appName, tableId, orderedDefns, elementKeyToValue, params, rowId); if (!writeDataSuccessful) { WebLogger.getLogger(appName).e(TAG, "could not write instance file successfully!"); } Uri insertUri = CollectUtil.getUriForCollectInstanceForRowData(context, appName, tableId, params, rowId, shouldUpdate); // Copied the below from getIntentForOdkCollectEditRow(). Intent intent = new Intent(); intent.setComponent(new ComponentName("org.odk.collect.android", "org.odk.collect.android.activities.FormEntryActivity")); intent.setAction(Intent.ACTION_EDIT); intent.setData(insertUri); // intent.putExtra("start", true); // jump right into form return intent; } /** * Launch collect with the given intent. This method should be used rather * than launching the activity yourself because the rowId needs to be retained * in order to update the database. * * @param activityToAwaitReturn * @param collectEditIntent * @param rowId */ public static void launchCollectToEditRow(Activity activityToAwaitReturn, Intent collectEditIntent, String rowId) { // We want to be able to launch an edit row action from a variety of // different activities, such as the spreadsheet and the webviews. In // order to update the database, we must know what the row id of the row // was which we are editing. There appears to be no way to pass this // information to collect and have it return it to us, so we're going to // store it in a shared preference. // // Note that we aren't storing this in the key value store because it is // a very temporary bit of state that would be meaningless if the call // and return to/from collect was interrupted. SharedPreferences preferences = activityToAwaitReturn.getSharedPreferences(SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); preferences.edit().putString(PREFERENCE_KEY_EDITED_ROW_ID, rowId).commit(); activityToAwaitReturn.startActivityForResult(collectEditIntent, Constants.RequestCodes.EDIT_ROW_COLLECT); } /** * Launch Collect with the given Intent. This method should be used rather * than launching the Intent yourself if the row is going to be added into a * table other than that which you are currently displaying. This method * handles storing the table id of that table so that it can be reclaimed when * the activity returns. * <p> * Launches with the return code * {@link Constants.RequestCodes.ADD_ROW_COLLECT}. * * @param activityToAwaitReturn * @param collectAddIntent * @param tableId */ public static void launchCollectToAddRow(Activity activityToAwaitReturn, Intent collectAddIntent, String tableId) { // We want to save the id of the table that is going to receive the row // that returns from Collect. We'll store it in a SharedPreference so // that we can get at it. SharedPreferences preferences = activityToAwaitReturn.getSharedPreferences(SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); preferences.edit().putString(PREFERENCE_KEY_TABLE_ID_ADD, tableId).commit(); activityToAwaitReturn.startActivityForResult(collectAddIntent, Constants.RequestCodes.ADD_ROW_COLLECT); } /** * This gets a map of values for insertion into a row after returning from a * Collect form. It handles validating the values. Null values are passed back * if the value is not present or is null in the return from ODK Collect (to * support clearing of values). * * TODO: add support for select-multiple * * @return */ public static ContentValues getMapForInsertion(Context context, String appName, String tableId, ArrayList<ColumnDefinition> orderedDefns, FormValues formValues) { DataUtil du = new DataUtil(Locale.ENGLISH, TimeZone.getDefault()); ContentValues values = new ContentValues(); List<ColumnDefinition> geopointList = GeoColumnUtil.get().getGeopointColumnDefinitions(orderedDefns); List<ColumnDefinition> uriList = RowPathColumnUtil.get().getUriColumnDefinitions(orderedDefns); for (ColumnDefinition cd : orderedDefns) { ColumnDefinition cdContainingElement = cd.getParent(); if (cdContainingElement != null) { if (geopointList.contains(cdContainingElement) || uriList.contains(cdContainingElement)) { // processed by the containing type continue; } // and if this is not a unit of retention, a containing element is // handling it. if (!cd.isUnitOfRetention()) { continue; } } // ok. we are directly processing this... and possibly sucking values // out of sub-elements... ElementType type = cd.getType(); if (geopointList.contains(cd)) { // find its children... List<ColumnDefinition> children = cd.getChildren(); ColumnDefinition[] cparray = new ColumnDefinition[4]; if (!children.isEmpty()) { cparray[0] = children.get(0); } if (children.size() > 1) { cparray[1] = children.get(1); } if (children.size() > 2) { cparray[2] = children.get(2); } if (children.size() > 3) { cparray[3] = children.get(3); } ColumnDefinition cplat = null, cplng = null, cpalt = null, cpacc = null; for (ColumnDefinition scp : cparray) { if (scp.getElementName().equals("latitude")) { cplat = scp; } else if (scp.getElementName().equals("longitude")) { cplng = scp; } else if (scp.getElementName().equals("altitude")) { cpalt = scp; } else if (scp.getElementName().equals("accuracy")) { cpacc = scp; } } // split ODK COLLECT value into the constituent elements String value = formValues.formValues.get(cd.getElementKey()); if (value == null || value.length() == 0) { values.putNull(cplat.getElementKey()); values.putNull(cplng.getElementKey()); values.putNull(cpalt.getElementKey()); values.putNull(cpacc.getElementKey()); } else { String[] parts = value.split(" "); if (parts.length > 0) { values.put(cplat.getElementKey(), parts[0]); } else { values.putNull(cplat.getElementKey()); } if (parts.length > 1) { values.put(cplng.getElementKey(), parts[1]); } else { values.putNull(cplng.getElementKey()); } if (parts.length > 2) { values.put(cpalt.getElementKey(), parts[2]); } else { values.putNull(cpalt.getElementKey()); } if (parts.length > 3) { values.put(cpacc.getElementKey(), parts[3]); } else { values.putNull(cpacc.getElementKey()); } } } else if (uriList.contains(cd)) { // find its children... List<ColumnDefinition> children = cd.getChildren(); ColumnDefinition[] cdarray = new ColumnDefinition[children.size()]; for (int i = 0; i < children.size(); ++i) { cdarray[i] = children.get(i); } // find the uriFragment ColumnDefinition cdfrag = null, cdtype = null; for (ColumnDefinition scp : cdarray) { if (scp.getElementName().equals("uriFragment")) { cdfrag = scp; } else if (scp.getElementName().equals("contentType")) { cdtype = scp; } } // update the uriFragment and contentType elements String value = formValues.formValues.get(cd.getElementKey()); if (value == null || value.length() == 0) { values.putNull(cdfrag.getElementKey()); values.putNull(cdtype.getElementKey()); } else { int dotIdx = value.lastIndexOf("."); String ext = (dotIdx == -1) ? "*" : value.substring(dotIdx + 1); if (ext.length() == 0) { ext = "*"; } String baseContentType = cd.getElementName().substring(0, cd.getElementName().length() - 3); if (baseContentType.equals("mime")) { baseContentType = "*"; } String mimeType = baseContentType + "/" + ext; if (cd.getType().getDataType() == ElementDataType.configpath) { values.put(cdfrag.getElementKey(), ODKFileUtils.asUriFragment(appName, new File(ODKFileUtils.getAppFolder(appName), value))); values.put(cdtype.getElementKey(), mimeType); } else { File ifolder = new File( ODKFileUtils.getInstanceFolder(appName, tableId, formValues.instanceID)); values.put(cdfrag.getElementKey(), ODKFileUtils.asUriFragment(appName, new File(ifolder, value))); values.put(cdtype.getElementKey(), mimeType); } } } else if (cd.isUnitOfRetention()) { ArrayList<Map<String, Object>> choices; SQLiteDatabase db = null; try { db = DatabaseFactory.get().getDatabase(context, appName); choices = (ArrayList<Map<String, Object>>) ColumnUtil.get().getDisplayChoicesList(db, tableId, cd.getElementKey()); } finally { if (db != null) { db.close(); } } String value = formValues.formValues.get(cd.getElementKey()); value = ParseUtil.validifyValue(appName, du, choices, cd, formValues.formValues.get(cd.getElementKey())); if (value != null) { values.put(cd.getElementKey(), value); } else { // don't we want to clear values too? values.putNull(cd.getElementKey()); } } } return values; } /** * Returns true if the instance has been marked as complete/finalized. If the * instance cannot be found or is not marked as complete, returns false. * * @param context * @param instanceId * @return */ private static boolean instanceIsFinalized(Context context, int instanceId) { String[] projection = { COLLECT_KEY_STATUS }; String selection = "_id = ?"; String[] selectionArgs = { instanceId + "" }; Cursor c = null; try { c = context.getContentResolver().query(COLLECT_INSTANCES_CONTENT_URI, projection, selection, selectionArgs, COLLECT_INSTANCE_ORDER_BY); if (c.getCount() == 0) { return false; } c.moveToFirst(); String status = ODKDatabaseUtils.get().getIndexAsString(c, c.getColumnIndexOrThrow(COLLECT_KEY_STATUS)); // potential status values are incomplete, complete, submitted, // submission_failed // all but the incomplete status indicate a marked-as-complete record. if (status != null && !status.equals(COLLECT_KEY_STATUS_INCOMPLETE)) { return true; } else { return false; } } finally { if (c != null && !c.isClosed()) { c.close(); } } } private static class FormValues { Map<String, String> formValues = new HashMap<String, String>(); Long timestamp; // should be endTime in form? String instanceID; String formId; String locale; String savepointCreator; FormValues() { }; }; /** * Return the Collect form values from the given instance id. * * @param context * @param instanceId * @return */ public static FormValues getOdkCollectFormValuesFromInstanceId(Context context, String appName, int instanceId) { String[] projection = { COLLECT_KEY_LAST_STATUS_CHANGE_DATE, "displayName", "instanceFilePath" }; String selection = "_id = ?"; String[] selectionArgs = { (instanceId + "") }; Cursor c = null; try { c = context.getContentResolver().query(COLLECT_INSTANCES_CONTENT_URI, projection, selection, selectionArgs, null); if (c.getCount() != 1) { return null; } c.moveToFirst(); FormValues fv = new FormValues(); fv.timestamp = ODKDatabaseUtils.get().getIndexAsType(c, Long.class, c.getColumnIndexOrThrow(COLLECT_KEY_LAST_STATUS_CHANGE_DATE)); String instancepath = ODKDatabaseUtils.get().getIndexAsString(c, c.getColumnIndexOrThrow("instanceFilePath")); File instanceFile = new File(instancepath); parseXML(appName, fv, instanceFile); return fv; } finally { if (c != null && !c.isClosed()) { c.close(); } } } /** * Retrieves the tableId that was stored during the call to * {@link CollectUtil#launchCollectToAddRow(Activity, Intent, String)} . * Removes the tableId so that future calls to the same method will return * null. * * @param context * @return the stored tableId, or null if no tableId was found. */ public static String retrieveAndRemoveTableIdForAddRow(Context context) { SharedPreferences sharedPreferences = context.getSharedPreferences(SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); String tableId = sharedPreferences.getString(PREFERENCE_KEY_TABLE_ID_ADD, null); sharedPreferences.edit().remove(PREFERENCE_KEY_TABLE_ID_ADD).commit(); return tableId; } private static boolean updateRowFromOdkCollectInstance(Context context, String appName, String tableId, int instanceId) { // First we need to check to make sure the row id is in the shared // preferences. If it's not, something has gone wrong. // TODO: This should be migrated to use metadata/instanceID in the // instance xpath. SharedPreferences sharedPreferences = context.getSharedPreferences(SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); String rowId = sharedPreferences.getString(PREFERENCE_KEY_EDITED_ROW_ID, null); if (rowId == null) { // Then it wasn't retained and something went wrong. WebLogger.getLogger(appName).e(TAG, "rowId retrieved from shared preferences was null."); return false; } FormValues formValues = CollectUtil.getOdkCollectFormValuesFromInstanceId(context, appName, instanceId); if (formValues == null) { return false; } ArrayList<ColumnDefinition> orderedDefns; SQLiteDatabase db = null; try { db = DatabaseFactory.get().getDatabase(context, appName); orderedDefns = TableUtil.get().getColumnDefinitions(db, appName, tableId); ContentValues values = CollectUtil.getMapForInsertion(context, appName, tableId, orderedDefns, formValues); values.put(DataTableColumns.ID, rowId); values.put(DataTableColumns.FORM_ID, formValues.formId); values.put(DataTableColumns.LOCALE, formValues.locale); values.put(DataTableColumns.SAVEPOINT_TYPE, SavepointTypeManipulator.complete()); values.put(DataTableColumns.SAVEPOINT_TIMESTAMP, TableConstants.nanoSecondsFromMillis(formValues.timestamp)); values.put(DataTableColumns.SAVEPOINT_CREATOR, formValues.savepointCreator); ODKDatabaseUtils.get().updateDataInExistingDBTableWithId(db, tableId, orderedDefns, values, rowId); } finally { if (db != null) { db.close(); } } // If we made it here and there were no errors, then clear the row id // from the shared preferences. This is just a bit of housekeeping that // will mean there's no you could accidentally wind up overwriting the // wrong row. sharedPreferences.edit().remove(PREFERENCE_KEY_EDITED_ROW_ID).commit(); return true; } /** * Returns false if the returnCode is not ok or if the instance pointed to by * the intent was not marked as finalized. * <p> * Otherwise returns the result of * {@link #updateRowFromOdkCollectInstance(Context, tableId, int)}. * * * @param context * @param appName * @param tableId * @param returnCode * @param data * @return */ public static boolean handleOdkCollectEditReturn(Context context, String appName, String tableId, int returnCode, Intent data) { if (returnCode != Activity.RESULT_OK) { WebLogger.getLogger(appName).i(TAG, "return code wasn't OK not inserting " + "edited data."); return false; } if (data.getData() == null) { WebLogger.getLogger(appName).i(TAG, "data was null --not editing row"); return false; } int instanceId = Integer.valueOf(data.getData().getLastPathSegment()); if (!instanceIsFinalized(context, instanceId)) { WebLogger.getLogger(appName).i(TAG, "instance wasn't marked as finalized--not updating"); return false; } return updateRowFromOdkCollectInstance(context, appName, tableId, instanceId); } /** * Returns false if the returnCode is not ok or if the instance pointed to by * the intent was not marked as finalized. * <p> * Otherwise returns the result of * {@link #addRowFromOdkCollectInstance(Context, String, String, int)}. * * @param context * @param appName * @param tableId * @param returnCode * @param data * @return */ public static boolean handleOdkCollectAddReturn(Context context, String appName, String tableId, int returnCode, Intent data) { if (returnCode != Activity.RESULT_OK) { WebLogger.getLogger(appName).i(TAG, "return code wasn't OK --not adding row"); return false; } if (data.getData() == null) { WebLogger.getLogger(appName).i(TAG, "data was null --not adding row"); return false; } int instanceId = Integer.valueOf(data.getData().getLastPathSegment()); if (!instanceIsFinalized(context, instanceId)) { WebLogger.getLogger(appName).i(TAG, "instance wasn't finalized--not adding"); return false; } return addRowFromOdkCollectInstance(context, appName, tableId, instanceId); } private static boolean addRowFromOdkCollectInstance(Context context, String appName, String tableId, int instanceId) { FormValues formValues = CollectUtil.getOdkCollectFormValuesFromInstanceId(context, appName, instanceId); if (formValues == null) { return false; } ArrayList<ColumnDefinition> orderedDefns; SQLiteDatabase db = null; try { db = DatabaseFactory.get().getDatabase(context, appName); orderedDefns = TableUtil.get().getColumnDefinitions(db, appName, tableId); ContentValues values = CollectUtil.getMapForInsertion(context, appName, tableId, orderedDefns, formValues); values.put(DataTableColumns.ID, formValues.instanceID); values.put(DataTableColumns.FORM_ID, formValues.formId); values.put(DataTableColumns.LOCALE, formValues.locale); values.put(DataTableColumns.SAVEPOINT_TYPE, SavepointTypeManipulator.complete()); values.put(DataTableColumns.SAVEPOINT_TIMESTAMP, TableConstants.nanoSecondsFromMillis(formValues.timestamp)); values.put(DataTableColumns.SAVEPOINT_CREATOR, formValues.savepointCreator); ODKDatabaseUtils.get().insertDataIntoExistingDBTableWithId(db, tableId, orderedDefns, values, formValues.instanceID); } finally { if (db != null) { db.close(); } } return true; } public static Intent getIntentForOdkCollectAddRowByQuery(Context context, String appName, String tableId, ArrayList<ColumnDefinition> orderedDefns, CollectFormParameters params) { Intent intentAddRow = CollectUtil.getIntentForOdkCollectAddRow(context, appName, tableId, orderedDefns, params, null); return intentAddRow; } /** * Return an intent that can be launched to add a row. * * @param context * @param tp * @param params * @param elementKeyToValue * values with which you want to prepopulate the add row form. * @return */ public static Intent getIntentForOdkCollectAddRow(Context context, String appName, String tableId, ArrayList<ColumnDefinition> orderedDefns, CollectFormParameters params, Map<String, String> elementKeyToValue) { /* * So, there are several things to check here. The first thing we want to do * is see if a custom form has been defined for this table. If there is not, * then we will need to write a custom one. When we do this, we will then * have to call delete on Collect to remove the old form, which may have * used the same id. This will not fail if a form has not been already been * written--delete will simply return 0. */ // Check if there is a custom form. If there is not, we want to delete // the old form and write the new form. if (!params.isCustom()) { boolean formIsReady = CollectUtil.deleteWriteAndInsertFormIntoCollect(context, appName, tableId, orderedDefns, params); if (!formIsReady) { WebLogger.getLogger(appName).e(TAG, "could not delete, write, or insert a generated form"); return null; } } // manufacture a rowId for this record... String rowId = "uuid:" + UUID.randomUUID().toString(); boolean shouldUpdate = CollectUtil.isExistingCollectInstanceForRowData(context, appName, tableId, rowId); // emit the empty or partially-populated instance // we've received some values to prepopulate the add row with. boolean writeDataSuccessful = CollectUtil.writeRowDataToBeEdited(context, appName, tableId, orderedDefns, elementKeyToValue, params, rowId); if (!writeDataSuccessful) { WebLogger.getLogger(appName).e(TAG, "could not write instance file successfully!"); } // Here we'll just act as if we're inserting 0, which // really doesn't matter? Uri formToLaunch = CollectUtil.getUriForCollectInstanceForRowData(context, appName, tableId, params, rowId, shouldUpdate); // And now finally create the intent. Intent intent = new Intent(); intent.setComponent(new ComponentName("org.odk.collect.android", "org.odk.collect.android.activities.FormEntryActivity")); intent.setAction(Intent.ACTION_EDIT); intent.setData(formToLaunch); intent.putExtra("start", true); // jump right into form return intent; } /** * Parse the given xml file and return a map of element to value. * <p> * Based on Collect's {@code parseXML} in {@code FileUtils}. * * @param xmlFile * @return */ private static void parseXML(String appName, FormValues fv, File xmlFile) { InputStream is; try { is = new FileInputStream(xmlFile); } catch (FileNotFoundException e) { throw new IllegalStateException(e); } // Now get the reader. InputStreamReader isr; try { isr = new InputStreamReader(is, Charset.forName(CharEncoding.UTF_8)); } catch (UnsupportedCharsetException e) { WebLogger.getLogger(appName).w(TAG, "UTF-8 wasn't supported--trying with default charset"); isr = new InputStreamReader(is); } if (isr != null) { Document document; try { document = new Document(); KXmlParser parser = new KXmlParser(); try { parser.setInput(isr); document.parse(parser); } catch (XmlPullParserException e) { WebLogger.getLogger(appName).e(TAG, "problem with xmlpullparse"); WebLogger.getLogger(appName).printStackTrace(e); } catch (IOException e) { WebLogger.getLogger(appName).e(TAG, "io exception when parsing"); WebLogger.getLogger(appName).printStackTrace(e); } } finally { try { isr.close(); } catch (IOException e) { WebLogger.getLogger(appName).e(TAG, "couldn't close reader"); WebLogger.getLogger(appName).printStackTrace(e); } } Element rootEl = document.getRootElement(); fv.locale = Locale.getDefault().getLanguage(); fv.formId = rootEl.getAttributeValue(null, "id"); Node rootNode = rootEl.getRoot(); Element dataEl = rootNode.getElement(0); for (int i = 0; i < dataEl.getChildCount(); i++) { Element child = dataEl.getElement(i); String key = child.getName(); if (key.equals("meta")) { for (int j = 0; j < child.getChildCount(); j++) { Element e = child.getElement(j); String name = e.getName(); if (name.equals("instanceID")) { fv.instanceID = ODKFileUtils.getXMLText(e, false); } } } else { String value = ODKFileUtils.getXMLText(child, false); fv.formValues.put(key, value); } } } } /** * This is holds the most basic information needed to define a form to be * opened by Collect. Essentially it wraps the formVersion, formId, and * formXMLRootElement. * <p> * Its accessor methods return the default values or the set values as * appropriate, so that calling the getters will be safe and there will be no * need for checking returned values for null or whatever else. * <p> * At least at the moment, this is conceptualized to exist alongside the * current interaction with Collect, which writes out a complete form that * includes every column in the database on a single swipe through screen. * This instead is supposed to fill a pre-defined form that has been set by * the user. * * @author sudar.sam@gmail.com * */ public static class CollectFormParameters { private String mFormId; private String mFormVersion; private String mFormXMLRootElement; private String mRowDisplayName; private boolean mIsCustom; @SuppressWarnings("unused") private CollectFormParameters() { // Just putting this here in case it needs to be serialized at some point // and someone forgets about this requirement. } /** * Create an object housing parameters for a Collect form. Very important is * the isCustom parameter, which should be true is a custom form has been * defined, and false otherwise. This will have implications for which forms * are used and deleted and created, and is very important to get right. * * @param isCustom * @param formId * @param formVersion * @param formXMLRootElement */ public CollectFormParameters(boolean isCustom, String formId, String formVersion, String formXMLRootElement, String rowDisplayName) { this.mIsCustom = isCustom; this.mFormId = formId; this.mFormVersion = formVersion; this.mFormXMLRootElement = formXMLRootElement; this.mRowDisplayName = rowDisplayName; } public static CollectFormParameters constructDefaultCollectFormParameters(Context context, String appName, String tableId) { String localizedDisplayName; SQLiteDatabase db = null; try { db = DatabaseFactory.get().getDatabase(context, appName); localizedDisplayName = TableUtil.get().getLocalizedDisplayName(db, tableId); } finally { if (db != null) { db.close(); } } return new CollectFormParameters(false, getDefaultAddRowFormId(tableId), null, DEFAULT_ROOT_ELEMENT, localizedDisplayName); } /** * Construct a CollectFormProperties object from the given tableId. The * object is determined to have custom parameters if a formId can be * retrieved for this tableId. Otherwise the default addrow parameters are * set. If no formVersion is defined, it is left as null, as later on a * check is used that if none is defined (ie is null), do not insert it to a * map. If no root element is defined, the default root element is added. * <p> * The display name of the row will be the display name of the table. * * @param context * @param appName * @param tableId * @return */ public static CollectFormParameters constructCollectFormParameters(Context context, String appName, String tableId) { String formId; String formVersion = null; String rootElement = null; String localizedDisplayName; SQLiteDatabase db = null; try { db = DatabaseFactory.get().getDatabase(context, appName); localizedDisplayName = TableUtil.get().getLocalizedDisplayName(db, tableId); KeyValueStoreHelper kvsh = new KeyValueStoreHelper(db, tableId, CollectUtil.KVS_PARTITION); KeyValueHelper aspectHelper = kvsh.getAspectHelper(CollectUtil.KVS_ASPECT); formId = aspectHelper.getString(CollectUtil.KEY_FORM_ID); if (formId != null) { formVersion = aspectHelper.getString(CollectUtil.KEY_FORM_VERSION); rootElement = aspectHelper.getString(CollectUtil.KEY_FORM_ROOT_ELEMENT); } } finally { if (db != null) { db.close(); } } if (formId == null) { return new CollectFormParameters(false, getDefaultAddRowFormId(tableId), null, DEFAULT_ROOT_ELEMENT, localizedDisplayName); } // Else we know it is custom. if (rootElement == null) { rootElement = DEFAULT_ROOT_ELEMENT; } return new CollectFormParameters(true, formId, formVersion, rootElement, localizedDisplayName); } public void persist(SQLiteDatabase db, String tableId) { KeyValueStoreHelper kvsh = new KeyValueStoreHelper(db, tableId, CollectUtil.KVS_PARTITION); KeyValueHelper aspectHelper = kvsh.getAspectHelper(CollectUtil.KVS_ASPECT); if (this.isCustom()) { aspectHelper.setString(CollectUtil.KEY_FORM_ID, this.mFormId); aspectHelper.setString(CollectUtil.KEY_FORM_VERSION, this.mFormVersion); aspectHelper.setString(CollectUtil.KEY_FORM_ROOT_ELEMENT, this.mFormXMLRootElement); } else { aspectHelper.removeKey(CollectUtil.KEY_FORM_ID); aspectHelper.removeKey(CollectUtil.KEY_FORM_VERSION); aspectHelper.removeKey(CollectUtil.KEY_FORM_ROOT_ELEMENT); } } /** * Sets the form id and marks the form as custom. * * @param formId */ public void setFormId(String formId) { this.mFormId = formId; this.mIsCustom = true; } /** * Sets the form version and marks the form as custom. * * @param formVersion */ public void setFormVersion(String formVersion) { this.mFormVersion = formVersion; this.mIsCustom = true; } /** * Sets the root element and marks the form as custom. * * @param rootElement */ public void setRootElement(String rootElement) { this.mFormXMLRootElement = rootElement; this.mIsCustom = true; } /** * Sets the row display name and marks the form as custom. * * @param name */ public void setRowDisplayName(String name) { this.mRowDisplayName = name; this.mIsCustom = true; } public void setIsCustom(boolean isCustom) { this.mIsCustom = isCustom; } public boolean isCustom() { return this.mIsCustom; } /** * Return the root element of the form to be used for writing. If none has * been set, returns the {@link DEFAULT_ROOT_ELEMENT}. * * @return */ public String getRootElement() { if (this.mFormXMLRootElement == null) { return DEFAULT_ROOT_ELEMENT; } else { return this.mFormXMLRootElement; } } /** * Return the form version. This does not do any null checking. A null value * means that no form version has been specified and it should just be * omitted. * * @return */ public String getFormVersion() { return this.mFormVersion; } /** * Return the ID of the form. * * @return */ public String getFormId() { return this.mFormId; } public String getRowDisplayName() { return this.mRowDisplayName; } } }