/*
* Copyright (C) 2009 University of Washington
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.radicaldynamic.groupinform.tasks;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.ektorp.AttachmentInputStream;
import org.javarosa.core.model.FormDef;
import org.javarosa.core.model.FormIndex;
import org.javarosa.core.services.transport.payload.ByteArrayPayload;
import org.javarosa.form.api.FormEntryController;
import org.javarosa.model.xform.XFormSerializingVisitor;
import org.odk.collect.android.logic.FormController;
import org.odk.collect.android.utilities.FileUtils;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;
import android.webkit.MimeTypeMap;
import com.radicaldynamic.groupinform.activities.FormEntryActivity;
import com.radicaldynamic.groupinform.application.Collect;
import com.radicaldynamic.groupinform.documents.FormInstance;
import com.radicaldynamic.groupinform.listeners.FormSavedListener;
/**
* Background task for loading a form.
*
* @author Carl Hartung (carlhartung@gmail.com)
* @author Yaw Anokwa (yanokwa@gmail.com)
*/
public class SaveToDiskTask extends AsyncTask<Void, String, Integer> {
private final static String t = "SaveToDiskTask";
private FormSavedListener mSavedListener;
private Boolean mSave;
private Boolean mMarkCompleted;
@SuppressWarnings("unused")
private Uri mUri;
private String mInstanceName;
public static final int SAVED = 500;
public static final int SAVE_ERROR = 501;
public static final int VALIDATE_ERROR = 502;
public static final int VALIDATED = 503;
public static final int SAVED_AND_EXIT = 504;
// BEGIN custom
FormInstance mFormInstance;
// END custom
// BEGIN custom
// public SaveToDiskTask(Uri uri, Boolean saveAndExit, Boolean markCompleted, String updatedName) {
public SaveToDiskTask(Uri uri, Boolean saveAndExit, Boolean markCompleted, String updatedName, FormInstance formInstance) {
// END custom
mUri = uri;
mSave = saveAndExit;
mMarkCompleted = markCompleted;
mInstanceName = updatedName;
// BEGIN custom
mFormInstance = formInstance;
// END custom
}
/**
* Initialize {@link FormEntryController} with {@link FormDef} from binary or from XML. If given
* an instance, it will be used to fill the {@link FormDef}.
*/
@Override
protected Integer doInBackground(Void... nothing) {
// validation failed, pass specific failure
int validateStatus = validateAnswers(mMarkCompleted);
if (validateStatus != VALIDATED) {
return validateStatus;
}
FormEntryActivity.mFormController.postProcessInstance();
if (mSave && exportData(mMarkCompleted)) {
return SAVED_AND_EXIT;
} else if (exportData(mMarkCompleted)) {
return SAVED;
}
return SAVE_ERROR;
}
/**
* Write's the data to the sdcard, and updates the instances content provider.
* In theory we don't have to write to disk, and this is where you'd add
* other methods.
* @param markCompleted
* @return
*/
public boolean exportData(boolean markCompleted) {
ByteArrayPayload payload;
try {
// assume no binary data inside the model.
// BEGIN custom
// FormInstance datamodel = FormEntryActivity.mFormController.getInstance();
org.javarosa.core.model.instance.FormInstance datamodel = FormEntryActivity.mFormController.getInstance();
// END custom
XFormSerializingVisitor serializer = new XFormSerializingVisitor();
payload = (ByteArrayPayload) serializer.createSerializedPayload(datamodel);
// write out xml
exportXmlFile(payload, FormEntryActivity.mInstancePath);
} catch (IOException e) {
if (Collect.Log.ERROR) Log.e(t, "Error creating serialized payload");
e.printStackTrace();
return false;
}
// BEGIN custom
// // If FormEntryActivity was started with an Instance, just update that instance
// if (Collect.getInstance().getContentResolver().getType(mUri) == InstanceColumns.CONTENT_ITEM_TYPE) {
// ContentValues values = new ContentValues();
// if (mInstanceName != null) {
// values.put(InstanceColumns.DISPLAY_NAME, mInstanceName);
// }
// if (!mMarkCompleted) {
// values.put(InstanceColumns.STATUS, InstanceProviderAPI.STATUS_INCOMPLETE);
// } else {
// values.put(InstanceColumns.STATUS, InstanceProviderAPI.STATUS_COMPLETE);
// }
// Collect.getInstance().getContentResolver().update(mUri, values, null, null);
// } else if (Collect.getInstance().getContentResolver().getType(mUri) == FormsColumns.CONTENT_ITEM_TYPE) {
// // If FormEntryActivity was started with a form, then it's likely the first time we're
// // saving.
// // However, it could be a not-first time saving if the user has been using the manual
// // 'save data' option from the menu. So try to update first, then make a new one if that
// // fails.
// ContentValues values = new ContentValues();
// if (mInstanceName != null) {
// values.put(InstanceColumns.DISPLAY_NAME, mInstanceName);
// }
// if (mMarkCompleted) {
// values.put(InstanceColumns.STATUS, InstanceProviderAPI.STATUS_COMPLETE);
// } else {
// values.put(InstanceColumns.STATUS, InstanceProviderAPI.STATUS_INCOMPLETE);
// }
//
// String where = InstanceColumns.INSTANCE_FILE_PATH + "=?";
// String[] whereArgs = {
// FormEntryActivity.mInstancePath
// };
// int updated =
// Collect.getInstance().getContentResolver()
// .update(InstanceColumns.CONTENT_URI, values, where, whereArgs);
// if (updated > 1) {
// Log.w(t, "Updated more than one entry, that's not good");
// } else if (updated == 1) {
// Log.i(t, "Instance already exists, updating");
// // already existed and updated just fine
// return true;
// } else {
// Log.e(t, "No instance found, creating");
// // Entry didn't exist, so create it.
// Cursor c =
// Collect.getInstance().getContentResolver().query(mUri, null, null, null, null);
// c.moveToFirst();
// String jrformid = c.getString(c.getColumnIndex(FormsColumns.JR_FORM_ID));
// String formname = c.getString(c.getColumnIndex(FormsColumns.DISPLAY_NAME));
// String submissionUri = c.getString(c.getColumnIndex(FormsColumns.SUBMISSION_URI));
//
// values.put(InstanceColumns.INSTANCE_FILE_PATH, FormEntryActivity.mInstancePath);
// values.put(InstanceColumns.INSTANCE_FILE_PATH, FormEntryActivity.mInstancePath);
// values.put(InstanceColumns.SUBMISSION_URI, submissionUri);
// if (mInstanceName != null) {
// values.put(InstanceColumns.DISPLAY_NAME, mInstanceName);
// } else {
// values.put(InstanceColumns.DISPLAY_NAME, formname);
// }
// values.put(InstanceColumns.JR_FORM_ID, jrformid);
// Collect.getInstance().getContentResolver()
// .insert(InstanceColumns.CONTENT_URI, values);
// }
// }
String instancePath = FormEntryActivity.mInstancePath;
String docId = instancePath.substring(instancePath.lastIndexOf("/") + 1, instancePath.lastIndexOf("."));
boolean firstSave = mFormInstance.getStatus().equals(FormInstance.Status.placeholder);
String revision = null;
try {
// Update document status, etc.
if (mMarkCompleted)
mFormInstance.setStatus(FormInstance.Status.complete);
else
mFormInstance.setStatus(FormInstance.Status.draft);
mFormInstance.setXmlHash(FileUtils.getMd5Hash(new File(instancePath)));
mFormInstance.setName(mInstanceName);
Collect.getInstance().getDbService().getDb().update(mFormInstance);
revision = mFormInstance.getRevision();
// Process attachments
File instanceDir = new File(instancePath).getParentFile();
String[] attachmentFilenames = instanceDir.list();
/*
* Get a list of attachments that already exist; we will use this to determine if
* we need to remove attachments after saving attachments that exist in our cache
* directory.
*/
Set<String> existingAttachments = new HashSet<String>();
if (mFormInstance.getAttachments() != null) {
existingAttachments = new HashSet<String>(mFormInstance.getAttachments().keySet());
}
for (String fileName : attachmentFilenames) {
File f = new File(new File(instancePath).getParentFile(), fileName);
String ext = fileName.substring(fileName.lastIndexOf(".") + 1);
boolean exists = false;
// XML instance file should simply be named "xml"
if (fileName.equals(docId + ".xml"))
fileName = "xml";
// Remove this entry from the list of existing attachments (to be removed)
if (existingAttachments.contains(fileName))
exists = existingAttachments.remove(fileName);
// Skip identically named attachments with the same size
if (exists && mFormInstance.getAttachments().get(fileName).getContentLength() == f.length()) {
Log.v(Collect.LOGTAG, t + ": " + fileName + " already attached to " + docId + " (" + f.length() + " bytes)");
// Don't skip the xml instance document! Contents are likely to change but leave the size untouched.
if (!fileName.equals("xml"))
continue;
} else {
Log.d(Collect.LOGTAG, t + ": attaching " + fileName + " to " + docId);
}
FileInputStream fis = new FileInputStream(f);
String contentType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
AttachmentInputStream a = new AttachmentInputStream(fileName, fis, contentType, f.length());
revision = Collect.getInstance().getDbService().getDb().createAttachment(docId, revision, a);
}
// Remove attachments that no longer exist
Iterator<String> attachmentsToRemove = existingAttachments.iterator();
while (attachmentsToRemove.hasNext()) {
String attachmentId = attachmentsToRemove.next();
try {
Log.d(Collect.LOGTAG, t + ": removing unused attachment " + attachmentId);
revision = Collect.getInstance().getDbService().getDb().deleteAttachment(docId, revision, attachmentId);
} catch (Exception e) {
if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + ": unexpected exception while removing unused attachment " + attachmentId);
e.printStackTrace();
}
}
// Make sure that we have the most current instance to return to FormEntryActivity
mFormInstance = Collect.getInstance().getDbService().getDb().get(FormInstance.class, docId);
} catch (Exception e) {
if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + ": unexpected exception while attaching files to instance document " + docId);
e.printStackTrace();
/*
* It is possible that the first update may succeed but that attachments changes may fail.
* If this document is a new one, then remove it altogether so that we don't have stale
* and invalid documents laying about. This is a last ditch effort.
*/
try {
if (firstSave) {
Log.d(Collect.LOGTAG, t + ": removing " + mFormInstance.getId() + " due to first save failure ");
Collect.getInstance().getDbService().getDb().delete(mFormInstance.getId(), mFormInstance.getRevision());
mFormInstance = Collect.getInstance().getDbService().getDb().get(FormInstance.class, docId);
}
} catch (Exception e1) {
if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + ": failed last ditch effort to tidy after save oops");
e.printStackTrace();
}
return false;
}
// END custom
return true;
}
/**
* This method actually writes the xml to disk.
* @param payload
* @param path
* @return
*/
private boolean exportXmlFile(ByteArrayPayload payload, String path) {
// create data stream
InputStream is = payload.getPayloadStream();
int len = (int) payload.getLength();
// read from data stream
byte[] data = new byte[len];
try {
int read = is.read(data, 0, len);
if (read > 0) {
// write xml file
try {
// String filename = path + "/" +
// path.substring(path.lastIndexOf('/') + 1) + ".xml";
BufferedWriter bw = new BufferedWriter(new FileWriter(path));
bw.write(new String(data, "UTF-8"));
bw.flush();
bw.close();
return true;
} catch (IOException e) {
Log.e(t, "Error writing XML file");
e.printStackTrace();
return false;
}
}
} catch (IOException e) {
Log.e(t, "Error reading from payload data stream");
e.printStackTrace();
return false;
}
return false;
}
@Override
protected void onPostExecute(Integer result) {
synchronized (this) {
// BEGIN custom
// if (mSavedListener != null)
// mSavedListener.savingComplete(result);
if (mSavedListener != null)
mSavedListener.savingComplete(result, mFormInstance);
// END custom
}
}
public void setFormSavedListener(FormSavedListener fsl) {
synchronized (this) {
mSavedListener = fsl;
}
}
/**
* Goes through the entire form to make sure all entered answers comply with their constraints.
* Constraints are ignored on 'jump to', so answers can be outside of constraints. We don't
* allow saving to disk, though, until all answers conform to their constraints/requirements.
*
* @param markCompleted
* @return validatedStatus
*/
private int validateAnswers(Boolean markCompleted) {
FormIndex i = FormEntryActivity.mFormController.getFormIndex();
FormEntryActivity.mFormController.jumpToIndex(FormIndex.createBeginningOfFormIndex());
int event;
while ((event =
FormEntryActivity.mFormController.stepToNextEvent(FormController.STEP_OVER_GROUP)) != FormEntryController.EVENT_END_OF_FORM) {
if (event != FormEntryController.EVENT_QUESTION) {
continue;
} else {
int saveStatus =
FormEntryActivity.mFormController
.answerQuestion(FormEntryActivity.mFormController.getQuestionPrompt()
.getAnswerValue());
if (markCompleted && saveStatus != FormEntryController.ANSWER_OK) {
return saveStatus;
}
}
}
FormEntryActivity.mFormController.jumpToIndex(i);
return VALIDATED;
}
}