package org.commcare.android.tasks; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Calendar; import java.util.Vector; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.crypto.Cipher; 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.CommCareWiFiDirectActivity; 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.content.Context; import android.os.Environment; import android.util.Log; import android.widget.TextView; /** * @author ctsims * */ public abstract class ZipTask extends CommCareTask<String, String, FormRecord[], CommCareWiFiDirectActivity>{ Context c; Long[] results; File dumpFolder; public static final long FULL_SUCCESS = 0; public static final long PARTIAL_SUCCESS = 1; public static final long FAILURE = 2; public static final long TRANSPORT_FAILURE = 4; public static final long PROGRESS_ALL_PROCESSED = 8; 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 ZIP_TASK_ID = 72135; DataSubmissionListener formSubmissionListener; CommCarePlatform platform; SqlStorage<FormRecord> storage; private static long MAX_BYTES = (5 * 1048576)-1024; // 5MB less 1KB overhead public ZipTask(Context c, CommCarePlatform platform) throws SessionUnavailableException{ this.c = c; storage = CommCareApplication._().getUserStorage(FormRecord.class); taskId = ZIP_TASK_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(FormRecord[] 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{ Log.d(CommCareWiFiDirectActivity.TAG, "trying zip copy2"); FileUtil.copyFile(f, new File(myDir, f.getName()), decrypter, null); } catch(IOException ie){ Log.d(CommCareWiFiDirectActivity.TAG, "faield zip copywith2: " + f.getName()); publishProgress(("File writing failed: " + ie.getMessage())); return FormUploadUtil.FAILURE; } } else{ try{ Log.d(CommCareWiFiDirectActivity.TAG, "trying zip copy2"); FileUtil.copyFile(f, new File(myDir, f.getName())); } catch(IOException ie){ Log.d(CommCareWiFiDirectActivity.TAG, "faield zip copy2 " + f.getName() + "with messageL " + ie.getMessage()); publishProgress(("File writing failed: " + ie.getMessage())); return FormUploadUtil.FAILURE; } } } return FormUploadUtil.FULL_SUCCESS; } private boolean zipTargetFolder(File targetFilePath, String zipFile) throws IOException{ Log.d(CommCareWiFiDirectActivity.TAG, "827 zipTarggetFolder with tfp: " + targetFilePath.toString()+ ", zipFile: "+ zipFile); ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFile))); try{ if(!targetFilePath.isDirectory()){ System.out.println("827: target was not folder, bad"); } File[] fileArray = targetFilePath.listFiles(); for(int i=0;i<fileArray.length;i++){ File[] subFileArray = fileArray[i].listFiles(); zipFolder(subFileArray, zipFile, out); } }finally{ out.close(); } return false; } private boolean zipFolder(File[] files, String zipFile, ZipOutputStream zos) throws IOException { System.out.println("827 zipping folder with files: " +files[0]+ ", zipFile: " + zipFile); int BUFFER_SIZE = 1024; BufferedInputStream origin = null; byte data[] = new byte[BUFFER_SIZE]; for (int i = 0; i < files.length; i++) { FileInputStream fi = new FileInputStream(files[i]); origin = new BufferedInputStream(fi, BUFFER_SIZE); try { String tempPath = files[i].getPath(); System.out.println("827 zipping folder with path: " + tempPath); String[] pathParts = tempPath.split("/"); int pathPartsLength = pathParts.length; String fileName = pathParts[pathPartsLength-1]; String fileFolder = pathParts[pathPartsLength-2]; System.out.println("827 zipping folder with path: " + fileFolder + "/" + fileName); ZipEntry entry = new ZipEntry(fileFolder + "/" + fileName); zos.putNextEntry(entry); int count; while ((count = origin.read(data, 0, BUFFER_SIZE)) != -1) { zos.write(data, 0, count); } } finally { origin.close(); } } return false; } /* * (non-Javadoc) * @see org.commcare.android.tasks.templates.CommCareTask#doTaskBackground(java.lang.Object[]) */ @Override protected FormRecord[] doTaskBackground(String... params) { Log.d(CommCareWiFiDirectActivity.TAG, "doing zip task in background"); // 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 null; } if(!mExternalStorageWriteable){ publishProgress(Localization.get("bulk.form.sd.unwritable")); return null; } if(mExternalStorageEmulated && externalMounts.size() == 0){ publishProgress(Localization.get("bulk.form.sd.emulated")); return null; } File baseDirectory = new File(CommCareWiFiDirectActivity.baseDirectory); File sourceDirectory = new File(CommCareWiFiDirectActivity.sourceDirectory); if(baseDirectory.exists() && baseDirectory.isDirectory()){ baseDirectory.delete(); } baseDirectory.mkdirs(); sourceDirectory.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 = sourceDirectory; 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, platform, record); //publishProgress(Localization.get("bulk.form.dialog.progress",new String[]{""+i, ""+results[i].intValue()})); } if(results[i].intValue() == FormUploadUtil.FAILURE) { publishProgress("Failure during zipping process"); return null; } } } 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]; } } if(result == 0){ try{ System.out.println("827 trying zip"); String zipPath = CommCareWiFiDirectActivity.sourceDirectory; File nf = new File(zipPath); if(nf.exists()){ nf.delete(); } zipTargetFolder(nf, CommCareWiFiDirectActivity.sourceZipDirectory); sourceDirectory.delete(); }catch( IOException ioe){ System.out.println("827 IOException: " + ioe.getMessage()); } } //this.endSubmissionProcess(); } catch(SessionUnavailableException sue) { this.cancel(false); return null; } // // return records; } else { publishProgress("No forms to send."); return null; } } 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)); } }