package org.commcare.tasks;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import android.util.Pair;
import org.commcare.CommCareApplication;
import org.commcare.activities.CommCareWiFiDirectActivity;
import org.commcare.android.database.user.models.FormRecord;
import org.commcare.dalvik.R;
import org.commcare.logging.AndroidLogger;
import org.commcare.models.database.SqlStorage;
import org.commcare.preferences.CommCareServerPreferences;
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.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.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.Properties;
import java.util.Vector;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
/**
* This class transfers all of the FormRecords from form record storage and into our file system,
* decrypting files as necessary. This task should really be merged with DumpTask.
*
* @author wspride
*/
public abstract class FormRecordToFileTask extends CommCareTask<String, String, Pair<FormUploadResult, FormRecord[]>, CommCareWiFiDirectActivity> {
private static final String TAG = AndroidLogger.TYPE_FORM_DUMP;
private final Context c;
// this is where the forms that have been pulled from FormRecord storage to the file system live
private final File storedFormDirectory;
public static final int PULL_TASK_ID = 721356;
private static final String[] SUPPORTED_FILE_EXTS = {".xml", ".jpg", ".3gpp", ".3gp"};
public FormRecordToFileTask(Context c, String formStoragePath) {
this.c = c;
this.storedFormDirectory = new File(formStoragePath);
taskId = PULL_TASK_ID;
}
/**
* Turn a FormRecord folder from storage into a standard file representation in our file system.
* Return an int status code from FormUploadUtil corresponding to the outcome of the transfer
*/
private FormUploadResult copyFileInstanceFromStorage(File formRecordFolder, SecretKeySpec decryptionKey) {
File[] files = formRecordFolder.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isFile();
}
});
Logger.log(TAG, "Trying to get instance with: " + files.length + " files.");
File myDir = new File(storedFormDirectory, formRecordFolder.getName());
myDir.mkdirs();
logTransferBytes(files);
final Cipher decryptCipher = FormUploadUtil.getDecryptCipher(decryptionKey);
try {
decryptCopyFiles(files, myDir, decryptCipher);
} catch (IOException e){
Log.d(TAG, "Copying file failed with: " + e.getMessage());
publishProgress(("File writing failed: " + e.getMessage()));
return FormUploadResult.FAILURE;
}
// write any form.properties we want
writeProperties(myDir);
return FormUploadResult.FULL_SUCCESS;
}
private void decryptCopyFiles(File[] files, File targetDirectory, Cipher decryptCipher) throws IOException{
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")) {
FileUtil.copyFile(file, new File(targetDirectory, file.getName()), decryptCipher, null);
} else {
FileUtil.copyFile(file, new File(targetDirectory, file.getName()));
}
}
}
private static boolean isSupportedFiletype(File file){
for (String ext : SUPPORTED_FILE_EXTS) {
if (file.getName().endsWith(ext)) {
return true;
}
}
return false;
}
private static void logTransferBytes(File[] files){
long bytes = 0;
for (File file : files) {
//Make sure we'll be sending it
if (!isSupportedFiletype(file)) {
continue;
}
bytes += file.length();
}
Log.d(TAG, "Storing " + bytes + " form bytes");
}
/**
* Writes any properties of this form/user the receiving tablet might want to form.properties
* Current properties:
* PostURL: The receiver will attempt to submit to this URL instead of its default URL.
* We do this because HQ uses the receiver URL to help display forms prettily.
* @param formInstanceFolder: the form instance folder to write in
*/
private void writeProperties(File formInstanceFolder) {
FileOutputStream outputStream = null;
try {
File formProperties = new File(formInstanceFolder, "form.properties");
outputStream = new FileOutputStream(formProperties);
Properties properties = new Properties();
SharedPreferences settings = CommCareApplication.instance().getCurrentApp().getAppPreferences();
// HQ likes us to submit forms to the "correct" app and user specific URL
String postUrl = settings.getString(CommCareServerPreferences.PREFS_SUBMISSION_URL_KEY,
c.getString(R.string.PostURL));
properties.setProperty("PostURL", postUrl);
properties.store(outputStream, null);
} catch(IOException e){
// we'll just ignore this, not the end of the world
e.printStackTrace();
} finally{
if(outputStream != null){
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
@Override
protected Pair<FormUploadResult, FormRecord[]> doTaskBackground(String... params) {
Log.d(TAG, "Doing zip task in background with params: " + Arrays.toString(params));
FormUploadResult[] results;
// we want this directory to be clean
if (storedFormDirectory.exists()) {
storedFormDirectory.delete();
}
storedFormDirectory.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()];
results = new FormUploadResult[records.length];
for (int i = 0; i < ids.size(); ++i) {
records[i] = storage.read(ids.elementAt(i));
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 transfer forms to storage!
results[i] = copyFileInstanceFromStorage(folder, new SecretKeySpec(record.getAesKey(), "AES"));
if (results[i] == FormUploadResult.FAILURE) {
publishProgress("Failure during zipping process");
}
}
} 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));
}
}
return new Pair<>(FormUploadResult.getWorstResult(results), records);
} else {
publishProgress(Localization.get("form.transfer.no.forms"));
return null;
}
}
private static 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));
}
}