package org.commcare.android.tasks; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; import java.util.LinkedList; import java.util.Queue; import javax.crypto.spec.SecretKeySpec; import org.commcare.android.database.user.models.FormRecord; import org.commcare.android.database.user.models.User; import org.commcare.android.javarosa.AndroidLogger; import org.commcare.android.models.logic.FormRecordProcessor; import org.commcare.android.models.notifications.MessageTag; import org.commcare.android.models.notifications.NotificationMessageFactory; import org.commcare.android.tasks.templates.CommCareTask; import org.commcare.android.util.FormUploadUtil; import org.commcare.android.util.SessionUnavailableException; import org.commcare.dalvik.activities.LoginActivity; import org.commcare.dalvik.application.CommCareApplication; import org.commcare.suite.model.Profile; import org.commcare.util.CommCarePlatform; import org.commcare.xml.util.InvalidStructureException; import org.commcare.xml.util.UnfullfilledRequirementsException; import org.javarosa.core.services.Logger; import org.javarosa.core.services.storage.StorageFullException; import org.xmlpull.v1.XmlPullParserException; import android.content.Context; import android.os.AsyncTask; /** * @author ctsims * */ public abstract class ProcessAndSendTask<R> extends CommCareTask<FormRecord, Long, Integer, R> implements DataSubmissionListener { Context c; String url; Long[] results; int sendTaskId; public enum ProcessIssues implements MessageTag { /** Logs successfully submitted **/ BadTransactions("notification.processing.badstructure"), /** Logs saved, but not actually submitted **/ StorageRemoved("notification.processing.nosdcard"), /** You were logged out while something was occurring **/ LoggedOut("notification.sending.loggedout", LoginActivity.NOTIFICATION_MESSAGE_LOGIN), /** Logs saved, but not actually submitted **/ RecordQuarantined("notification.sending.quarantine"); ProcessIssues(String root) {this(root, "processing");} ProcessIssues(String root, String category) {this.root = root;this.category = category;} private final String root, category; public String getLocaleKeyBase() { return root;} public String getCategory() { return category; } } public static final int PROCESSING_PHASE_ID = 8; public static final int SEND_PHASE_ID = 9; public static final long PROGRESS_ALL_PROCESSED = 8; public static final long SUBMISSION_BEGIN = 16; public static final long SUBMISSION_START = 32; public static final long SUBMISSION_NOTIFY = 64; public static final long SUBMISSION_DONE = 128; public static final long PROGRESS_LOGGED_OUT = 256; public static final long PROGRESS_SDCARD_REMOVED = 512; DataSubmissionListener formSubmissionListener; CommCarePlatform platform; private FormRecordProcessor processor; private static int SUBMISSION_ATTEMPTS = 2; static Queue<ProcessAndSendTask> processTasks = new LinkedList<ProcessAndSendTask>(); private static long MAX_BYTES = (5 * 1048576)-1024; // 5MB less 1KB overhead public ProcessAndSendTask(Context c, String url) throws SessionUnavailableException{ this(c, url, SEND_PHASE_ID, true); } public ProcessAndSendTask(Context c, String url, int sendTaskId, boolean inSyncMode) throws SessionUnavailableException{ this.c = c; this.url = url; this.sendTaskId = sendTaskId; this.processor = new FormRecordProcessor(c); if (inSyncMode) { this.taskId = PROCESSING_PHASE_ID; } else { this.taskId = -1; } } /* (non-Javadoc) * @see android.os.AsyncTask#doInBackground(Params[]) */ protected Integer doTaskBackground(FormRecord... records) { boolean needToSendLogs = false; try { results = new Long[records.length]; for(int i = 0; i < records.length ; ++i ) { //Assume failure results[i] = FormUploadUtil.FAILURE; } //The first thing we need to do is make sure everything is processed, //we can't actually proceed before that. for(int i = 0 ; i < records.length ; ++i) { FormRecord record = records[i]; //If the form is complete, but unprocessed, process it. if(FormRecord.STATUS_COMPLETE.equals(record.getStatus())) { try { records[i] = processor.process(record); } catch (InvalidStructureException e) { CommCareApplication._().reportNotificationMessage(NotificationMessageFactory.message(ProcessIssues.BadTransactions), true); Logger.log(AndroidLogger.TYPE_ERROR_DESIGN, "Removing form record due to transaction data|" + getExceptionText(e)); FormRecordCleanupTask.wipeRecord(c, record); needToSendLogs = true; continue; } catch (XmlPullParserException e) { CommCareApplication._().reportNotificationMessage(NotificationMessageFactory.message(ProcessIssues.BadTransactions), true); Logger.log(AndroidLogger.TYPE_ERROR_DESIGN, "Removing form record due to bad xml|" + getExceptionText(e)); FormRecordCleanupTask.wipeRecord(c, record); needToSendLogs = true; continue; } catch (UnfullfilledRequirementsException e) { CommCareApplication._().reportNotificationMessage(NotificationMessageFactory.message(ProcessIssues.BadTransactions), true); Logger.log(AndroidLogger.TYPE_ERROR_DESIGN, "Removing form record due to bad requirements|" + getExceptionText(e)); FormRecordCleanupTask.wipeRecord(c, record); needToSendLogs = true; continue; } catch (FileNotFoundException e) { if(CommCareApplication._().isStorageAvailable()) { //If storage is available generally, this is a bug in the app design Logger.log(AndroidLogger.TYPE_ERROR_DESIGN, "Removing form record because file was missing|" + getExceptionText(e)); FormRecordCleanupTask.wipeRecord(c, record); } else { CommCareApplication._().reportNotificationMessage(NotificationMessageFactory.message(ProcessIssues.StorageRemoved), true); //Otherwise, the SD card just got removed, and we need to bail anyway. return (int)PROGRESS_SDCARD_REMOVED; } continue; } catch (IOException e) { Logger.log(AndroidLogger.TYPE_ERROR_WORKFLOW, "IO Issues processing a form. Tentatively not removing in case they are resolvable|" + getExceptionText(e)); continue; } } } this.publishProgress(PROGRESS_ALL_PROCESSED); //Put us on the queue! synchronized(processTasks) { processTasks.add(this); } boolean proceed = false; boolean needToRefresh = false; while(!proceed) { //TODO: Terrible? //See if it's our turn to go synchronized(processTasks) { //Are we at the head of the queue? ProcessAndSendTask head = processTasks.peek(); if(processTasks.peek() == this) { proceed = true; break; } //Otherwise, is the head of the queue busted? //*sigh*. Apparently Cancelled doesn't result in the task status being set //to !Running for reasons which baffle me. if(head.getStatus() != AsyncTask.Status.RUNNING || head.isCancelled()) { //If so, get rid of it processTasks.remove(head); } } //If it's not yet quite our turn, take a nap try { needToRefresh = true; Thread.sleep(500); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(needToRefresh) { //There was another activity before this one. Refresh our models in case //they were updated for(int i = 0; i < records.length; ++i ){ int dbId = records[i].getID(); records[i] = processor.getRecord(dbId); } } //Ok, all forms are now processed. Time to focus on sending if(formSubmissionListener != null) { formSubmissionListener.beginSubmissionProcess(records.length); } for(int i = 0 ; i < records.length ; ++i) { //See whether we are OK to proceed based on the last form. We're now guaranteeing //that forms are sent in order, so we won't proceed unless we succeed. We'll also permit //proceeding if there was a local problem with a record, since we'll just move on from that //processing. if(i > 0 && !(results[i - 1] == FormUploadUtil.FULL_SUCCESS || results[i - 1] == FormUploadUtil.RECORD_FAILURE)) { //Something went wrong with the last form, so we need to cancel this whole shebang Logger.log(AndroidLogger.TYPE_WARNING_NETWORK, "Cancelling submission due to network errors. " + (i - 1) + " forms succesfully sent."); break; } FormRecord record = records[i]; try{ //If it's unsent, go ahead and send it if(FormRecord.STATUS_UNSENT.equals(record.getStatus())) { File folder; try { folder = new File(record.getPath(c)).getCanonicalFile().getParentFile(); } catch (IOException e) { Logger.log(AndroidLogger.TYPE_ERROR_WORKFLOW, "Bizarre. Exception just getting the file reference. Not removing." + getExceptionText(e)); continue; } //Good! //Time to Send! try { User mUser = CommCareApplication._().getSession().getLoggedInUser(); int attemptsMade = 0; while(attemptsMade < SUBMISSION_ATTEMPTS) { if(attemptsMade > 0) { Logger.log(AndroidLogger.TYPE_WARNING_NETWORK, "Retrying submission. " + (SUBMISSION_ATTEMPTS - attemptsMade) + " attempts remain"); } results[i] = FormUploadUtil.sendInstance(i, folder, new SecretKeySpec(record.getAesKey(), "AES"), url, this, mUser); if(results[i] == FormUploadUtil.FULL_SUCCESS) { break; } else { attemptsMade++; } } if(results[i] == FormUploadUtil.RECORD_FAILURE) { //We tried to submit multiple times and there was a local problem (not a remote problem). //This implies that something is wrong with the current record, and we need to quarantine it. processor.updateRecordStatus(record, FormRecord.STATUS_LIMBO); Logger.log(AndroidLogger.TYPE_ERROR_STORAGE, "Quarantined Form Record"); CommCareApplication._().reportNotificationMessage(NotificationMessageFactory.message(ProcessIssues.RecordQuarantined), true); } } catch (FileNotFoundException e) { if(CommCareApplication._().isStorageAvailable()) { //If storage is available generally, this is a bug in the app design Logger.log(AndroidLogger.TYPE_ERROR_DESIGN, "Removing form record because file was missing|" + getExceptionText(e)); FormRecordCleanupTask.wipeRecord(c, record); } else { //Otherwise, the SD card just got removed, and we need to bail anyway. CommCareApplication._().reportNotificationMessage(NotificationMessageFactory.message(ProcessIssues.StorageRemoved), true); break; } continue; } Profile p = CommCareApplication._().getCommCarePlatform().getCurrentProfile(); //Check for success if(results[i].intValue() == FormUploadUtil.FULL_SUCCESS) { //Only delete if this device isn't set up to review. if(p == null || !p.isFeatureActive(Profile.FEATURE_REVIEW)) { FormRecordCleanupTask.wipeRecord(c, record); } else { //Otherwise save and move appropriately processor.updateRecordStatus(record, FormRecord.STATUS_SAVED); } } } else { results[i] = FormUploadUtil.FULL_SUCCESS; } } catch (StorageFullException e) { Logger.log(AndroidLogger.TYPE_ERROR_WORKFLOW, "Really? Storage full?" + getExceptionText(e)); throw new RuntimeException(e); } catch(SessionUnavailableException sue) { throw sue; } catch (Exception e) { //Just try to skip for now. Hopefully this doesn't wreck the model :/ Logger.log(AndroidLogger.TYPE_ERROR_DESIGN, "Totally Unexpected Error during form submission" + getExceptionText(e)); continue; } } long result = 0; for(int i = 0 ; i < records.length ; ++ i) { if(results[i] > result) { result = results[i]; } } return (int)result; } catch(SessionUnavailableException sue) { this.cancel(false); return (int)PROGRESS_LOGGED_OUT; } finally { this.endSubmissionProcess(); synchronized(processTasks) { processTasks.remove(this); } if(needToSendLogs) { CommCareApplication._().notifyLogsPending(); } } } public static int pending() { synchronized(processTasks) { return processTasks.size(); } } /* (non-Javadoc) * @see android.os.AsyncTask#onProgressUpdate(Progress[]) */ protected void onProgressUpdate(Long... values) { if(values.length == 1 && values[0] == ProcessAndSendTask.PROGRESS_ALL_PROCESSED) { this.transitionPhase(sendTaskId); } super.onProgressUpdate(values); if(values.length > 0 ) { if(formSubmissionListener != null) { //Parcel updates out if(values[0] == SUBMISSION_BEGIN) { formSubmissionListener.beginSubmissionProcess(values[1].intValue()); } else if(values[0] == SUBMISSION_START) { int item = values[1].intValue(); long size = values[2]; formSubmissionListener.startSubmission(item, size); } else if(values[0] == SUBMISSION_NOTIFY) { int item = values[1].intValue(); long progress = values[2]; formSubmissionListener.notifyProgress(item, progress); } else if(values[0] == SUBMISSION_DONE) { formSubmissionListener.endSubmissionProcess(); } } } } public void setListeners(DataSubmissionListener submissionListener) { this.formSubmissionListener = submissionListener; } /* * (non-Javadoc) * @see org.commcare.android.tasks.templates.CommCareTask#onPostExecute(java.lang.Object) */ @Override protected void onPostExecute(Integer result) { super.onPostExecute(result); //These will never get Zero'd otherwise c = null; url = null; results = null; } protected int getSuccesfulSends() { int successes = 0; for(Long formResult : results) { if(formResult != null && FormUploadUtil.FULL_SUCCESS == formResult.intValue()) { successes ++; } } return successes; } //Wrappers for the internal stuff public void beginSubmissionProcess(int totalItems) { this.publishProgress(SUBMISSION_BEGIN, (long)totalItems); } public void startSubmission(int itemNumber, long length) { // TODO Auto-generated method stub this.publishProgress(SUBMISSION_START, (long)itemNumber, length); } public void notifyProgress(int itemNumber, long progress) { this.publishProgress(SUBMISSION_NOTIFY, (long)itemNumber, progress); } public void endSubmissionProcess() { this.publishProgress(SUBMISSION_DONE); } private String getExceptionText (Exception e) { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); e.printStackTrace(new PrintStream(bos)); return new String(bos.toByteArray()); } catch(Exception ex) { return null; } } /* (non-Javadoc) * @see android.os.AsyncTask#onCancelled() */ @Override protected void onCancelled() { super.onCancelled(); if(this.formSubmissionListener != null) { formSubmissionListener.endSubmissionProcess(); } CommCareApplication._().reportNotificationMessage(NotificationMessageFactory.message(ProcessIssues.LoggedOut)); } }