/**
*
*/
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.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Vector;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import org.commcare.android.database.SqlStorage;
import org.commcare.android.database.user.models.FormRecord;
import org.commcare.android.javarosa.AndroidLogger;
import org.commcare.android.models.notifications.NotificationMessageFactory;
import org.commcare.android.tasks.ProcessAndSendTask.ProcessIssues;
import org.commcare.android.tasks.templates.CommCareTask;
import org.commcare.android.util.FileUtil;
import org.commcare.android.util.FormUploadUtil;
import org.commcare.android.util.ReflectionUtil;
import org.commcare.android.util.SessionUnavailableException;
import org.commcare.dalvik.activities.CommCareFormDumpActivity;
import org.commcare.dalvik.application.CommCareApplication;
import org.commcare.util.CommCarePlatform;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.locale.Localization;
import org.javarosa.core.services.storage.StorageFullException;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Environment;
import android.widget.TextView;
/**
* @author ctsims
*
*/
public abstract class DumpTask extends CommCareTask<String, String, Boolean, CommCareFormDumpActivity>{
Context c;
Long[] results;
File dumpFolder;
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;
public static final int BULK_DUMP_ID = 23456;
DataSubmissionListener formSubmissionListener;
CommCarePlatform platform;
SqlStorage<FormRecord> storage;
TextView outputTextView;
private static long MAX_BYTES = (5 * 1048576)-1024; // 5MB less 1KB overhead
public DumpTask(Context c, CommCarePlatform platform, TextView outputTextView) throws SessionUnavailableException{
this.c = c;
storage = CommCareApplication._().getUserStorage(FormRecord.class);
this.outputTextView = outputTextView;
taskId = DumpTask.BULK_DUMP_ID;
platform = this.platform;
}
/* (non-Javadoc)
* @see android.os.AsyncTask#onProgressUpdate(Progress[])
*/
protected void onProgressUpdate(String... values) {
super.onProgressUpdate(values);
}
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(Boolean result) {
super.onPostExecute(result);
//These will never get Zero'd otherwise
c = null;
results = null;
}
private static final String[] SUPPORTED_FILE_EXTS = {".xml", ".jpg", ".3gpp", ".3gp"};
private long dumpInstance(int submissionNumber, File folder, SecretKeySpec key) throws FileNotFoundException {
File[] files = folder.listFiles();
File myDir = new File(dumpFolder, folder.getName());
myDir.mkdirs();
if(files == null) {
//make sure external storage is available to begin with.
String state = Environment.getExternalStorageState();
if (!Environment.MEDIA_MOUNTED.equals(state)) {
//If so, just bail as if the user had logged out.
throw new SessionUnavailableException("External Storage Removed");
} else {
throw new FileNotFoundException("No directory found at: " + folder.getAbsoluteFile());
}
}
//If we're listening, figure out how much (roughly) we have to send
long bytes = 0;
for (int j = 0; j < files.length; j++) {
//Make sure we'll be sending it
boolean supported = false;
for(String ext : SUPPORTED_FILE_EXTS) {
if(files[j].getName().endsWith(ext)) {
supported = true;
break;
}
}
if(!supported) { continue;}
bytes += files[j].length();
}
//this.startSubmission(submissionNumber, bytes);
final Cipher decrypter = FormUploadUtil.getDecryptCipher(key);
for(int j=0;j<files.length;j++){
File f = files[j];
// This is not the ideal long term solution for determining whether we need decryption, but works
if (f.getName().endsWith(".xml")) {
try{
FileUtil.copyFile(f, new File(myDir, f.getName()), decrypter, null);
}
catch(IOException ie){
publishProgress(("File writing failed: " + ie.getMessage()));
return FormUploadUtil.FAILURE;
}
}
else{
try{
FileUtil.copyFile(f, new File(myDir, f.getName()));
}
catch(IOException ie){
publishProgress(("File writing failed: " + ie.getMessage()));
return FormUploadUtil.FAILURE;
}
}
}
return FormUploadUtil.FULL_SUCCESS;
}
/*
* (non-Javadoc)
* @see org.commcare.android.tasks.templates.CommCareTask#doTaskBackground(java.lang.Object[])
*/
@SuppressLint("NewApi")
@Override
protected Boolean doTaskBackground(String... params) {
// ensure that SD is available, writable, and not emulated
boolean mExternalStorageAvailable = false;
boolean mExternalStorageWriteable = false;
boolean mExternalStorageEmulated = ReflectionUtil.mIsExternalStorageEmulatedHelper();
String state = Environment.getExternalStorageState();
ArrayList<String> externalMounts = FileUtil.getExternalMounts();
if (Environment.MEDIA_MOUNTED.equals(state)) {
// We can read and write the media
mExternalStorageAvailable = mExternalStorageWriteable = true;
} else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
// We can only read the media
mExternalStorageAvailable = true;
mExternalStorageWriteable = false;
} else {
// Something else is wrong. It may be one of many other states, but all we need
// to know is we can neither read nor write
mExternalStorageAvailable = mExternalStorageWriteable = false;
}
if(!mExternalStorageAvailable){
publishProgress(Localization.get("bulk.form.sd.unavailable"));
return false;
}
if(!mExternalStorageWriteable){
publishProgress(Localization.get("bulk.form.sd.unwritable"));
return false;
}
if(mExternalStorageEmulated && externalMounts.size() == 0){
publishProgress(Localization.get("bulk.form.sd.emulated"));
return false;
}
String folderName = Localization.get("bulk.form.foldername");
String directoryPath = FileUtil.getDumpDirectory(c);
if(directoryPath == null){
publishProgress(Localization.get("bulk.form.sd.emulated"));
return false;
}
File dumpDirectory = new File(directoryPath+"/"+folderName);
if(dumpDirectory.exists() && dumpDirectory.isDirectory()){
dumpDirectory.delete();
}
dumpDirectory.mkdirs();
SqlStorage<FormRecord> storage = CommCareApplication._().getUserStorage(FormRecord.class);
//Get all forms which are either unsent or unprocessed
Vector<Integer> ids = storage.getIDsForValues(new String[] {FormRecord.META_STATUS}, new Object[] {FormRecord.STATUS_UNSENT});
ids.addAll(storage.getIDsForValues(new String[] {FormRecord.META_STATUS}, new Object[] {FormRecord.STATUS_COMPLETE}));
if(ids.size() > 0) {
FormRecord[] records = new FormRecord[ids.size()];
for(int i = 0 ; i < ids.size() ; ++i) {
records[i] = storage.read(ids.elementAt(i).intValue());
}
dumpFolder = dumpDirectory;
try{
results = new Long[records.length];
for(int i = 0; i < records.length ; ++i ) {
//Assume failure
results[i] = FormUploadUtil.FAILURE;
}
publishProgress(Localization.get("bulk.form.start"));
for(int i = 0 ; i < records.length ; ++i) {
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 {
results[i] = dumpInstance(i, folder, new SecretKeySpec(record.getAesKey(), "AES"));
} 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));
} else {
//Otherwise, the SD card just got removed, and we need to bail anyway.
CommCareApplication._().reportNotificationMessage(NotificationMessageFactory.message(ProcessIssues.StorageRemoved), true);
break;
}
continue;
}
//Check for success
if(results[i].intValue() == FormUploadUtil.FULL_SUCCESS) {
FormRecordCleanupTask.wipeRecord(c, record);
publishProgress(Localization.get("bulk.form.dialog.progress",new String[]{""+i, ""+results[i].intValue()}));
}
}
} 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];
}
}
//this.endSubmissionProcess();
}
catch(SessionUnavailableException sue) {
this.cancel(false);
return false;
}
//
//
return true;
} else {
publishProgress(Localization.get("bulk.form.no.unsynced"));
return false;
}
}
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));
}
}