package org.commcare.tasks;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Environment;
import org.commcare.CommCareApplication;
import org.commcare.activities.CommCareFormDumpActivity;
import org.commcare.android.database.user.models.FormRecord;
import org.commcare.logging.AndroidLogger;
import org.commcare.models.database.SqlStorage;
import org.commcare.tasks.templates.CommCareTask;
import org.commcare.utils.FileUtil;
import org.commcare.utils.FormUploadResult;
import org.commcare.utils.FormUploadUtil;
import org.commcare.utils.ReflectionUtil;
import org.commcare.utils.SessionUnavailableException;
import org.commcare.utils.StorageUtils;
import org.commcare.views.notifications.NotificationMessageFactory;
import org.commcare.views.notifications.ProcessIssues;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.locale.Localization;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Vector;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
/**
* @author ctsims
*/
public abstract class DumpTask extends CommCareTask<String, String, Boolean, CommCareFormDumpActivity>{
private Context c;
private FormUploadResult[] results;
private File dumpFolder;
private static final String TAG = DumpTask.class.getSimpleName();
public static final int BULK_DUMP_ID = 23456;
public DumpTask(Context c) {
this.c = c;
taskId = DumpTask.BULK_DUMP_ID;
}
@Override
protected void onProgressUpdate(String... values) {
super.onProgressUpdate(values);
}
@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 FormUploadResult dumpInstance(File folder, SecretKeySpec key) throws FileNotFoundException {
Logger.log(TAG, "Dumping form instance at folder: " + folder);
File[] files = folder.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isFile();
}
});
Logger.log(TAG, "Dumping files: " + Arrays.toString(files));
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 (File file : files) {
//Make sure we'll be sending it
boolean supported = false;
for (String ext : SUPPORTED_FILE_EXTS) {
if (file.getName().endsWith(ext)) {
supported = true;
break;
}
}
if (!supported) {
continue;
}
bytes += file.length();
}
//this.startSubmission(submissionNumber, bytes);
final Cipher decrypter = FormUploadUtil.getDecryptCipher(key);
for (File file : files) {
// This is not the ideal long term solution for determining whether we need decryption, but works
if (file.getName().endsWith(".xml")) {
try {
FileUtil.copyFile(file, new File(myDir, file.getName()), decrypter, null);
} catch (IOException ie) {
Logger.log(TAG, "Error copying file: " + file + " exception: " + ie.getMessage());
publishProgress(("File writing failed: " + ie.getMessage()));
return FormUploadResult.FAILURE;
}
} else {
try {
FileUtil.copyFile(file, new File(myDir, file.getName()));
} catch (IOException ie) {
Logger.log(TAG, "Error copying file: " + file + " exception: " + ie.getMessage());
publishProgress(("File writing failed: " + ie.getMessage()));
return FormUploadResult.FAILURE;
}
}
}
return FormUploadResult.FULL_SUCCESS;
}
@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.instance().getUserStorage(FormRecord.class);
Vector<Integer> ids = StorageUtils.getUnsentOrUnprocessedFormIdsForCurrentApp(storage);
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));
}
dumpFolder = dumpDirectory;
results = new FormUploadResult[records.length];
for (int i = 0; i < records.length; ++i) {
//Assume failure
results[i] = FormUploadResult.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(folder, new SecretKeySpec(record.getAesKey(), "AES"));
} catch (FileNotFoundException e) {
if (CommCareApplication.instance().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.notificationManager().reportNotificationMessage(NotificationMessageFactory.message(ProcessIssues.StorageRemoved), true);
break;
}
continue;
}
// Check for success
if (results[i] == FormUploadResult.FULL_SUCCESS) {
record.logPendingDeletion(TAG, "we are performing a form dump to external storage");
FormRecordCleanupTask.wipeRecord(c, record);
publishProgress(Localization.get("bulk.form.dialog.progress",new String[]{""+i, ""+results[i]}));
}
}
} catch (SessionUnavailableException sue) {
this.cancel(false);
return false;
} 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 dump task" + getExceptionText(e));
}
}
FormUploadResult result = FormUploadResult.getWorstResult(results);
return result == FormUploadResult.FULL_SUCCESS;
} 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;
}
}
@Override
protected void onCancelled() {
super.onCancelled();
CommCareApplication.notificationManager().reportNotificationMessage(NotificationMessageFactory.message(ProcessIssues.LoggedOut));
}
}