package org.commcare.android.tasks;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Date;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.entity.mime.MultipartEntity;
import org.commcare.android.database.SqlStorage;
import org.commcare.android.database.user.models.User;
import org.commcare.android.io.DataSubmissionEntity;
import org.commcare.android.javarosa.AndroidLogEntry;
import org.commcare.android.javarosa.AndroidLogSerializer;
import org.commcare.android.javarosa.AndroidLogger;
import org.commcare.android.javarosa.DeviceReportRecord;
import org.commcare.android.javarosa.DeviceReportWriter;
import org.commcare.android.mime.EncryptedFileBody;
import org.commcare.android.models.notifications.MessageTag;
import org.commcare.android.models.notifications.NotificationMessageFactory;
import org.commcare.android.net.HttpRequestGenerator;
import org.commcare.android.tasks.LogSubmissionTask.LogSubmitOutcomes;
import org.commcare.android.util.AndroidStreamUtil;
import org.commcare.android.util.SessionUnavailableException;
import org.commcare.dalvik.application.CommCareApplication;
import org.commcare.dalvik.preferences.CommCarePreferences;
import org.javarosa.core.services.Logger;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
/**
* @author ctsims
*
*/
public class LogSubmissionTask extends AsyncTask<Void, Long, LogSubmitOutcomes> implements DataSubmissionListener {
//Stole from the process and send task. See if we can unify a lot of this behavior
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 enum LogSubmitOutcomes implements MessageTag {
/** Logs successfully submitted **/
Submitted("notification.logger.submitted"),
/** Logs saved, but not actually submitted **/
Serialized("notification.logger.serialized"),
/** Something went wrong **/
Error("notification.logger.error");
LogSubmitOutcomes(String root) {this.root = root;}
private final String root;
public String getLocaleKeyBase() { return root;}
public String getCategory() { return "log_submission"; }
}
private Context c;
private boolean serializeCurrentLogs = false;
private DataSubmissionListener listener;
private String submissionUrl;
public LogSubmissionTask(Context c, boolean serializeCurrentLogs, DataSubmissionListener listener, String submissionUrl) {
this.c = c;
this.serializeCurrentLogs = serializeCurrentLogs;
this.listener = listener;
this.submissionUrl = submissionUrl;
}
/*
* (non-Javadoc)
* @see android.os.AsyncTask#doInBackground(java.lang.Object[])
*/
@Override
protected LogSubmitOutcomes doInBackground(Void... params) {
try {
SqlStorage<DeviceReportRecord> storage = CommCareApplication._().getUserStorage(DeviceReportRecord.class);
//First, see if we're supposed to serialize the current logs
if(serializeCurrentLogs) {
SharedPreferences settings = CommCareApplication._().getCurrentApp().getAppPreferences();
//update the last recorded record
settings.edit().putLong(CommCarePreferences.LOG_LAST_DAILY_SUBMIT, new Date().getTime()).commit();
//TODO: Test for logged in
DeviceReportRecord record = DeviceReportRecord.GenerateNewRecordStub();
//Ok, so first, we're going to write the logs to disk in an encrypted file
try {
DeviceReportWriter reporter;
try {
//Create a report writer
reporter = new DeviceReportWriter(record);
} catch(IOException e) {
//TODO: Bad local file (almost certainly). Throw a better message!
e.printStackTrace();
return LogSubmitOutcomes.Error;
}
//Add the logs as the primary payload
AndroidLogSerializer serializer = new AndroidLogSerializer(CommCareApplication._().getGlobalStorage(AndroidLogEntry.STORAGE_KEY, AndroidLogEntry.class));
reporter.addReportElement(serializer);
//serialize logs
reporter.write();
//Write the record for where the logs are now saved to.
storage.write(record);
//The logs are saved and recorded, so we can feel safe clearing the logs we serialized.
serializer.purge();
} catch (Exception e) {
//Bad times!
e.printStackTrace();
return LogSubmitOutcomes.Error;
}
}
//Alright, now regardless of whether or not we serialized one, we should see how many we have pending
//to submit.
int numberOfLogsToSubmit = storage.getNumRecords();
if(numberOfLogsToSubmit == 0) {
//Good to go.
return LogSubmitOutcomes.Submitted;
}
//Signal to the listener that we're ready to submit
this.beginSubmissionProcess(numberOfLogsToSubmit);
int index = 0;
ArrayList<Integer> submittedSuccesfullyIds = new ArrayList<Integer>();
ArrayList<DeviceReportRecord> submittedSuccesfully = new ArrayList<DeviceReportRecord>();
for(DeviceReportRecord slr : storage) {
try {
if(submit(slr, index)) {
submittedSuccesfullyIds.add(slr.getID());
submittedSuccesfully.add(slr);
}
index++;
} catch(Exception e) {
}
}
try {
//Wipe the DB entries
storage.remove(submittedSuccesfullyIds);
} catch(Exception e) {
e.printStackTrace();
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "Error deleting logs!" + e.getMessage());
return LogSubmitOutcomes.Serialized;
}
//Try to wipe the files, too, now that the file's submitted. (Not a huge deal if this fails, though)
for(DeviceReportRecord record : submittedSuccesfully) {
try{
File f = new File(record.getFilePath());
f.delete();
} catch(Exception e) {
//TODO: Anything useful here?
}
}
if(submittedSuccesfully.size() > 0) {
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "Succesfully submitted " + submittedSuccesfully.size() + " device reports to server.");
}
//Whether this is a full or partial success depends on how many logs were pending
if(submittedSuccesfully.size() == numberOfLogsToSubmit) {
//Submitted all the logs we had
return LogSubmitOutcomes.Submitted;
} else {
Logger.log(AndroidLogger.TYPE_MAINTENANCE, numberOfLogsToSubmit - submittedSuccesfully.size() + " logs remain on phone.");
//Some remain unsent
return LogSubmitOutcomes.Serialized;
}
} catch(SessionUnavailableException sue) {
return LogSubmitOutcomes.Error;
}
}
private boolean submit(DeviceReportRecord slr, int index) {
//Get our file pointer
File f = new File(slr.getFilePath());
/**
* Bad (Empty) record. Wipe
*/
if(f.length() == 0) {
return true;
}
//signal that it's time to start submitting the file
this.startSubmission(index, f.length());
HttpRequestGenerator generator;
User user = CommCareApplication._().getSession().getLoggedInUser();
if(user.getUserType().equals(User.TYPE_DEMO)) {
generator = new HttpRequestGenerator();
} else {
generator = new HttpRequestGenerator(user);
}
// mime post
MultipartEntity entity = new DataSubmissionEntity(this, index);
EncryptedFileBody fb = new EncryptedFileBody(f, getDecryptCipher(new SecretKeySpec(slr.getKey(), "AES")), "text/xml");
entity.addPart("xml_submission_file", fb);
HttpResponse response = null;
try {
response = generator.postData(submissionUrl, entity);
} catch (ClientProtocolException e) {
e.printStackTrace();
return false;
} catch (IOException e) {
e.printStackTrace();
return false;
} catch (IllegalStateException e) {
e.printStackTrace();
return false;
}
int responseCode = response.getStatusLine().getStatusCode();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
AndroidStreamUtil.writeFromInputToOutput(response.getEntity().getContent(), bos);
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//TODO: Anything with the response?
if(responseCode >= 200 && responseCode < 300) {
return true;
} else {
return false;
}
}
private static Cipher getDecryptCipher(SecretKeySpec key) {
Cipher cipher;
try {
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
return cipher;
//TODO: Something smart here;
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchPaddingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvalidKeyException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
/*
* (non-Javadoc)
* @see org.commcare.android.tasks.DataSubmissionListener#beginSubmissionProcess(int)
*/
@Override
public void beginSubmissionProcess(int totalItems) {
this.publishProgress(new Long[] {LogSubmissionTask.SUBMISSION_BEGIN, (long)totalItems});
}
/*
* (non-Javadoc)
* @see org.commcare.android.tasks.DataSubmissionListener#startSubmission(int, long)
*/
@Override
public void startSubmission(int itemNumber, long length) {
this.publishProgress(new Long[] {LogSubmissionTask.SUBMISSION_START, (long)itemNumber, length});
}
/*
* (non-Javadoc)
* @see org.commcare.android.tasks.DataSubmissionListener#notifyProgress(int, long)
*/
@Override
public void notifyProgress(int itemNumber, long progress) {
this.publishProgress(new Long[] {LogSubmissionTask.SUBMISSION_NOTIFY, (long)itemNumber, progress});
}
/*
* (non-Javadoc)
* @see org.commcare.android.tasks.DataSubmissionListener#endSubmissionProcess()
*/
@Override
public void endSubmissionProcess() {
this.publishProgress(new Long[] {LogSubmissionTask.SUBMISSION_DONE});
}
/* (non-Javadoc)
* @see android.os.AsyncTask#onProgressUpdate(Progress[])
*/
@Override
protected void onProgressUpdate(Long... values) {
super.onProgressUpdate(values);
if(values[0] == LogSubmissionTask.SUBMISSION_BEGIN) {
listener.beginSubmissionProcess(values[1].intValue());
} else if(values[0] == LogSubmissionTask.SUBMISSION_START) {
listener.startSubmission(values[1].intValue(), values[2]);
} else if(values[0] == LogSubmissionTask.SUBMISSION_NOTIFY) {
listener.notifyProgress(values[1].intValue(), values[2]);
} else if(values[0] == LogSubmissionTask.SUBMISSION_DONE) {
listener.endSubmissionProcess();
}
}
/* (non-Javadoc)
* @see android.os.AsyncTask#onPostExecute(java.lang.Object)
*/
@Override
protected void onPostExecute(LogSubmitOutcomes result) {
super.onPostExecute(result);
listener.endSubmissionProcess();
if(result != LogSubmitOutcomes.Submitted) {
CommCareApplication._().reportNotificationMessage(NotificationMessageFactory.message(result));
} else{
CommCareApplication._().clearNotifications(result.getCategory());
}
}
}