package org.commcare.tasks; import android.content.SharedPreferences; import android.os.AsyncTask; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.entity.ContentType; import org.apache.http.entity.mime.MultipartEntity; import org.commcare.CommCareApplication; import org.commcare.android.logging.ForceCloseLogEntry; import org.commcare.android.logging.ForceCloseLogSerializer; import org.commcare.android.javarosa.AndroidLogEntry; import org.commcare.logging.AndroidLogSerializer; import org.commcare.logging.AndroidLogger; import org.commcare.android.javarosa.DeviceReportRecord; import org.commcare.logging.DeviceReportWriter; import org.commcare.logging.XPathErrorEntry; import org.commcare.logging.XPathErrorSerializer; import org.commcare.models.database.SqlStorage; import org.commcare.network.DataSubmissionEntity; import org.commcare.network.EncryptedFileBody; import org.commcare.network.HttpRequestGenerator; import org.commcare.preferences.CommCarePreferences; import org.commcare.preferences.CommCareServerPreferences; import org.commcare.tasks.LogSubmissionTask.LogSubmitOutcomes; import org.commcare.utils.SessionUnavailableException; import org.commcare.views.notifications.MessageTag; import org.commcare.views.notifications.NotificationMessageFactory; import org.javarosa.core.model.User; import org.javarosa.core.services.Logger; 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; /** * @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 private static final long SUBMISSION_BEGIN = 16; private static final long SUBMISSION_START = 32; private static final long SUBMISSION_NOTIFY = 64; private static final long SUBMISSION_DONE = 128; protected 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; @Override public String getLocaleKeyBase() { return root; } @Override public String getCategory() { return "log_submission"; } } private boolean serializeCurrentLogs = false; private final DataSubmissionListener listener; private final String submissionUrl; public LogSubmissionTask(boolean serializeCurrentLogs, DataSubmissionListener listener, String submissionUrl) { this.serializeCurrentLogs = serializeCurrentLogs; this.listener = listener; this.submissionUrl = submissionUrl; } public static String getSubmissionUrl(SharedPreferences appPreferences) { return appPreferences.getString(CommCareServerPreferences.PREFS_LOG_POST_URL_KEY, appPreferences.getString(CommCareServerPreferences.PREFS_SUBMISSION_URL_KEY, null)); } @Override protected LogSubmitOutcomes doInBackground(Void... params) { try { SqlStorage<DeviceReportRecord> storage = CommCareApplication.instance().getUserStorage(DeviceReportRecord.class); if (serializeCurrentLogs && !serializeLogs(storage)) { return LogSubmitOutcomes.Error; } // See how many we have pending to submit int numberOfLogsToSubmit = storage.getNumRecords(); if (numberOfLogsToSubmit == 0) { return LogSubmitOutcomes.Submitted; } // Signal to the listener that we're ready to submit this.beginSubmissionProcess(numberOfLogsToSubmit); ArrayList<Integer> submittedSuccesfullyIds = new ArrayList<>(); ArrayList<DeviceReportRecord> submittedSuccesfully = new ArrayList<>(); submitReports(storage, submittedSuccesfullyIds, submittedSuccesfully); if (!removeLocalReports(storage, submittedSuccesfullyIds, submittedSuccesfully)) { return LogSubmitOutcomes.Serialized; } return checkSubmissionResult(numberOfLogsToSubmit, submittedSuccesfully); } catch (SessionUnavailableException e) { // The user database closed on us return LogSubmitOutcomes.Error; } } /** * Serialize all of the entries currently in Android logs, Xpath error logs, and Force close * logs, and write that to a DeviceReportRecord, which then gets added to the internal storage * object of all DeviceReportRecords that have yet to be submitted */ private boolean serializeLogs(SqlStorage<DeviceReportRecord> storage) { SharedPreferences settings = CommCareApplication.instance().getCurrentApp().getAppPreferences(); //update the last recorded record settings.edit().putLong(CommCarePreferences.LOG_LAST_DAILY_SUBMIT, new Date().getTime()).commit(); 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 false; } // Serialize regular and xpath error logs for the current user AndroidLogSerializer<AndroidLogEntry> userLogSerializer = new AndroidLogSerializer<>( CommCareApplication.instance().getUserStorage(AndroidLogEntry.STORAGE_KEY, AndroidLogEntry.class)); reporter.addReportElement(userLogSerializer); XPathErrorSerializer xpathErrorSerializer = new XPathErrorSerializer( CommCareApplication.instance().getUserStorage(XPathErrorEntry.STORAGE_KEY, XPathErrorEntry.class)); reporter.addReportElement(xpathErrorSerializer); // Serialize all force close logs -- these can exist in both user and global storage ForceCloseLogSerializer globalForceCloseSerializer = new ForceCloseLogSerializer( CommCareApplication.instance().getGlobalStorage(ForceCloseLogEntry.STORAGE_KEY, ForceCloseLogEntry.class)); reporter.addReportElement(globalForceCloseSerializer); ForceCloseLogSerializer userForceCloseSerializer = new ForceCloseLogSerializer( CommCareApplication.instance().getUserStorage(ForceCloseLogEntry.STORAGE_KEY, ForceCloseLogEntry.class)); reporter.addReportElement(userForceCloseSerializer); // TEMPORARILY ONLY - serialize all force close logs in the old format, so that HQ // still picks them up, until we start processing the new format AndroidLogSerializer<ForceCloseLogEntry> globalForceCloseSerializer_oldFormat = new AndroidLogSerializer<>( CommCareApplication.instance().getGlobalStorage(ForceCloseLogEntry.STORAGE_KEY, ForceCloseLogEntry.class)); reporter.addReportElement(globalForceCloseSerializer_oldFormat); AndroidLogSerializer<ForceCloseLogEntry> userForceCloseSerializer_oldFormat = new AndroidLogSerializer<>( CommCareApplication.instance().getUserStorage(ForceCloseLogEntry.STORAGE_KEY, ForceCloseLogEntry.class)); reporter.addReportElement(userForceCloseSerializer_oldFormat); // Serialize all logs currently in global storage, since we have no way to determine // which app they truly belong to AndroidLogSerializer globalLogSerializer = new AndroidLogSerializer( CommCareApplication.instance().getGlobalStorage(AndroidLogEntry.STORAGE_KEY, AndroidLogEntry.class)); reporter.addReportElement(globalLogSerializer); // Write serialized logs to the record reporter.write(); // Write this DeviceReportRecord to where all logs are saved to storage.write(record); // The logs are saved and recorded, so we can feel safe clearing the logs we serialized. userLogSerializer.purge(); globalLogSerializer.purge(); xpathErrorSerializer.purge(); globalForceCloseSerializer.purge(); userForceCloseSerializer.purge(); } catch (Exception e) { //Bad times! e.printStackTrace(); return false; } return true; } private void submitReports(SqlStorage<DeviceReportRecord> storage, ArrayList<Integer> submittedSuccesfullyIds, ArrayList<DeviceReportRecord> submittedSuccesfully) { int index = 0; for (DeviceReportRecord slr : storage) { try { if (submitDeviceReportRecord(slr, submissionUrl, this, index)) { submittedSuccesfullyIds.add(slr.getID()); submittedSuccesfully.add(slr); } index++; } catch (Exception e) { } } } private static boolean submitDeviceReportRecord(DeviceReportRecord slr, String submissionUrl, DataSubmissionListener listener, int index) { //Get our file pointer File f = new File(slr.getFilePath()); // Bad (Empty) record. Wipe if (f.length() == 0) { return true; } listener.startSubmission(index, f.length()); HttpRequestGenerator generator; User user; try { user = CommCareApplication.instance().getSession().getLoggedInUser(); } catch (SessionUnavailableException e) { // lost the session, so report failed submission return false; } generator = new HttpRequestGenerator(user); MultipartEntity entity = new DataSubmissionEntity(listener, index); EncryptedFileBody fb = new EncryptedFileBody(f, getDecryptCipher(new SecretKeySpec(slr.getKey(), "AES")), ContentType.TEXT_XML); entity.addPart("xml_submission_file", fb); HttpResponse response; 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(); return (responseCode >= 200 && responseCode < 300); } private static boolean removeLocalReports(SqlStorage<DeviceReportRecord> storage, ArrayList<Integer> submittedSuccesfullyIds, ArrayList<DeviceReportRecord> submittedSuccesfully) { try { //Wipe the DB entries storage.remove(submittedSuccesfullyIds); } catch (Exception e) { e.printStackTrace(); Logger.log(AndroidLogger.TYPE_MAINTENANCE, "Error deleting logs!" + e.getMessage()); return false; } //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? } } return true; } private LogSubmitOutcomes checkSubmissionResult(int numberOfLogsToSubmit, ArrayList<DeviceReportRecord> submittedSuccesfully) { 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) { return LogSubmitOutcomes.Submitted; } else { Logger.log(AndroidLogger.TYPE_MAINTENANCE, numberOfLogsToSubmit - submittedSuccesfully.size() + " logs remain on phone."); //Some remain unsent return LogSubmitOutcomes.Serialized; } } 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 | InvalidKeyException | NoSuchPaddingException e) { e.printStackTrace(); } return null; } @Override public void beginSubmissionProcess(int totalItems) { this.publishProgress(LogSubmissionTask.SUBMISSION_BEGIN, (long)totalItems); } @Override public void startSubmission(int itemNumber, long sizeOfItem) { this.publishProgress(LogSubmissionTask.SUBMISSION_START, (long)itemNumber, sizeOfItem); } @Override public void notifyProgress(int itemNumber, long progress) { this.publishProgress(LogSubmissionTask.SUBMISSION_NOTIFY, (long)itemNumber, progress); } @Override public void endSubmissionProcess(boolean success) { this.publishProgress(LogSubmissionTask.SUBMISSION_DONE); } @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(true); } } @Override protected void onPostExecute(LogSubmitOutcomes result) { super.onPostExecute(result); listener.endSubmissionProcess(LogSubmitOutcomes.Submitted.equals(result)); if (result != LogSubmitOutcomes.Submitted) { CommCareApplication.notificationManager().reportNotificationMessage(NotificationMessageFactory.message(result)); } else { CommCareApplication.notificationManager().clearNotifications(result.getCategory()); } } }