package co.smartreceipts.android.workers; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Typeface; import android.net.Uri; import android.os.AsyncTask; import android.support.annotation.NonNull; import android.widget.Toast; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.List; import co.smartreceipts.android.R; import co.smartreceipts.android.activities.NavigationHandler; import co.smartreceipts.android.filters.LegacyReceiptFilter; import co.smartreceipts.android.model.Column; import co.smartreceipts.android.model.ColumnDefinitions; import co.smartreceipts.android.model.Distance; import co.smartreceipts.android.model.Receipt; import co.smartreceipts.android.model.Trip; import co.smartreceipts.android.model.impl.columns.distance.DistanceColumnDefinitions; import co.smartreceipts.android.persistence.DatabaseHelper; import co.smartreceipts.android.persistence.PersistenceManager; import co.smartreceipts.android.settings.UserPreferenceManager; import co.smartreceipts.android.settings.catalog.UserPreference; import co.smartreceipts.android.utils.IntentUtils; import co.smartreceipts.android.utils.log.Logger; import co.smartreceipts.android.workers.reports.Report; import co.smartreceipts.android.workers.reports.ReportGenerationException; import co.smartreceipts.android.workers.reports.csv.CsvReportWriter; import co.smartreceipts.android.workers.reports.csv.CsvTableGenerator; import co.smartreceipts.android.workers.reports.formatting.SmartReceiptsFormattableString; import co.smartreceipts.android.workers.reports.pdf.PdfBoxFullPdfReport; import co.smartreceipts.android.workers.reports.pdf.PdfBoxImagesOnlyReport; import co.smartreceipts.android.workers.reports.pdf.misc.TooManyColumnsException; import wb.android.flex.Flex; import wb.android.storage.StorageManager; //TODO: Redo this class... Really sloppy public class EmailAssistant { private static final String DEVELOPER_EMAIL = "will.r.b" + "aumann" + "@" + "gm" + "ail" + "." + "com"; public enum EmailOptions { PDF_FULL(0), PDF_IMAGES_ONLY(1), CSV(2), ZIP_IMAGES_STAMPED(3); private final int index; EmailOptions(int index) { this.index = index; } public int getIndex() { return this.index; } } private final Context context; private final NavigationHandler navigationHandler; private final Flex flex; private final PersistenceManager persistenceManager; private final Trip trip; public static final Intent getEmailDeveloperIntent() { Intent intent = new Intent(Intent.ACTION_SENDTO); intent.setType("text/plain"); setEmailDeveloperRecipient(intent); intent.setData(Uri.parse("mailto:" + DEVELOPER_EMAIL)); return intent; } private static void setEmailDeveloperRecipient(Intent intent) { intent.setData(Uri.parse("mailto:" + DEVELOPER_EMAIL)); } public static final Intent getEmailDeveloperIntent(String subject) { Intent intent = getEmailDeveloperIntent(); intent.putExtra(Intent.EXTRA_SUBJECT, subject); return intent; } public static final Intent getEmailDeveloperIntent(String subject, String body) { Intent intent = getEmailDeveloperIntent(subject); intent.putExtra(Intent.EXTRA_TEXT, body); return intent; } public static final Intent getEmailDeveloperIntent(Context context, String subject, String body, List<File> files) { Intent intent = IntentUtils.getSendIntent(context, files); intent.putExtra(Intent.EXTRA_EMAIL, new String[]{DEVELOPER_EMAIL}); intent.putExtra(Intent.EXTRA_SUBJECT, subject); intent.putExtra(Intent.EXTRA_TEXT, body); return intent; } public EmailAssistant(NavigationHandler navigationHandler, Context context, Flex flex, PersistenceManager persistenceManager, Trip trip) { this.navigationHandler = navigationHandler; this.context = context; this.flex = flex; this.persistenceManager = persistenceManager; this.trip = trip; } public void emailTrip(@NonNull EnumSet<EmailOptions> options) { Logger.info(this, "Creating reports..."); ProgressDialog progress = ProgressDialog.show(context, "", "Building Reports...", true, false); EmailAttachmentWriter attachmentWriter = new EmailAttachmentWriter(persistenceManager, progress, options); attachmentWriter.execute(trip); } public void onAttachmentsCreated(File[] attachments) { List<File> files = new ArrayList<>(); StringBuilder bodyBuilder = new StringBuilder(); String path = ""; if (attachments[EmailOptions.PDF_FULL.getIndex()] != null) { path = attachments[EmailOptions.PDF_FULL.getIndex()].getParentFile().getAbsolutePath(); files.add(attachments[EmailOptions.PDF_FULL.getIndex()]); if (attachments[EmailOptions.PDF_FULL.getIndex()].length() > 5000000) { //Technically, this should be 5,242,880 but I'd rather give a warning buffer bodyBuilder.append("\n"); bodyBuilder.append(context.getString(R.string.email_body_subject_5mb_warning, attachments[EmailOptions.PDF_FULL.getIndex()].getAbsolutePath())); } } if (attachments[EmailOptions.PDF_IMAGES_ONLY.getIndex()] != null) { path = attachments[EmailOptions.PDF_IMAGES_ONLY.getIndex()].getParentFile().getAbsolutePath(); files.add(attachments[EmailOptions.PDF_IMAGES_ONLY.getIndex()]); if (attachments[EmailOptions.PDF_IMAGES_ONLY.getIndex()].length() > 5000000) { //Technically, this should be 5,242,880 but I'd rather give a warning buffer bodyBuilder.append("\n"); bodyBuilder.append(context.getString(R.string.email_body_subject_5mb_warning, attachments[EmailOptions.PDF_IMAGES_ONLY.getIndex()].getAbsolutePath())); } } if (attachments[EmailOptions.CSV.getIndex()] != null) { path = attachments[EmailOptions.CSV.getIndex()].getParentFile().getAbsolutePath(); files.add(attachments[EmailOptions.CSV.getIndex()]); if (attachments[EmailOptions.CSV.getIndex()].length() > 5000000) { //Technically, this should be 5,242,880 but I'd rather give a warning buffer bodyBuilder.append("\n"); bodyBuilder.append(context.getString(R.string.email_body_subject_5mb_warning, attachments[EmailOptions.CSV.getIndex()].getAbsolutePath())); } } if (attachments[EmailOptions.ZIP_IMAGES_STAMPED.getIndex()] != null) { path = attachments[EmailOptions.ZIP_IMAGES_STAMPED.getIndex()].getParentFile().getAbsolutePath(); files.add(attachments[EmailOptions.ZIP_IMAGES_STAMPED.getIndex()]); if (attachments[EmailOptions.ZIP_IMAGES_STAMPED.getIndex()].length() > 5000000) { //Technically, this should be 5,242,880 but I'd rather give a warning buffer bodyBuilder.append("\n"); bodyBuilder.append(context.getString(R.string.email_body_subject_5mb_warning, attachments[EmailOptions.ZIP_IMAGES_STAMPED.getIndex()].getAbsolutePath())); } } Logger.info(this, "Built the following files [{}].", files); String body = bodyBuilder.toString(); if (body.length() > 0) { body = "\n\n" + body; } if (files.size() == 1) { body = context.getString(R.string.report_attached) + body; } else if (files.size() > 1) { body = context.getString(R.string.reports_attached, Integer.toString(files.size())) + body; } final Intent emailIntent = IntentUtils.getSendIntent(context, files); final String[] to = persistenceManager.getPreferenceManager().get(UserPreference.Email.ToAddresses).split(";"); final String[] cc = persistenceManager.getPreferenceManager().get(UserPreference.Email.CcAddresses).split(";"); final String[] bcc = persistenceManager.getPreferenceManager().get(UserPreference.Email.BccAddresses).split(";"); emailIntent.putExtra(Intent.EXTRA_EMAIL, to); emailIntent.putExtra(Intent.EXTRA_CC, cc); emailIntent.putExtra(Intent.EXTRA_BCC, bcc); emailIntent.putExtra(Intent.EXTRA_SUBJECT, new SmartReceiptsFormattableString(persistenceManager.getPreferenceManager().get(UserPreference.Email.Subject), context, trip, persistenceManager.getPreferenceManager()).toString()); emailIntent.putExtra(Intent.EXTRA_TEXT, body); Logger.debug(this, "Built the send intent {} with extras {}.", emailIntent, emailIntent.getExtras()); try { context.startActivity(Intent.createChooser(emailIntent, context.getString(R.string.send_email))); } catch (ActivityNotFoundException e) { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.error_no_send_intent_dialog_title) .setMessage(context.getString(R.string.error_no_send_intent_dialog_message, path)) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }) .show(); } } public static final class WriterResults { public boolean didPDFFailCompletely = false; public boolean didPDFFailPartially = false; public boolean didPDFFailTooManyColumns = false; public boolean didSimplePDFFailCompletely = false; public boolean didSimplePDFFailPartially = false; public boolean didCSVFailCompletely = false; public boolean didCSVFailPartially = false; public boolean didZIPFailCompletely = false; public boolean didZIPFailPartially = false; public static final WriterResults getFullFailureInstance() { WriterResults result = new WriterResults(); result.didPDFFailCompletely = true; result.didPDFFailPartially = true; result.didPDFFailTooManyColumns = true; result.didSimplePDFFailCompletely = true; result.didSimplePDFFailPartially = true; result.didCSVFailCompletely = true; result.didCSVFailPartially = true; result.didZIPFailCompletely = true; result.didZIPFailPartially = true; return result; } } private class EmailAttachmentWriter extends AsyncTask<Trip, Integer, WriterResults> { private final StorageManager mStorageManager; private final DatabaseHelper mDB; private final UserPreferenceManager mPreferenceManager; private final WeakReference<ProgressDialog> mProgressDialog; private final File[] mFiles; private final EnumSet<EmailOptions> mOptions; private boolean memoryErrorOccured = false; public EmailAttachmentWriter(PersistenceManager persistenceManager, ProgressDialog dialog, EnumSet<EmailOptions> options) { mStorageManager = persistenceManager.getStorageManager(); mDB = persistenceManager.getDatabase(); mPreferenceManager = persistenceManager.getPreferenceManager(); mProgressDialog = new WeakReference<>(dialog); mOptions = options; mFiles = new File[]{null, null, null, null}; memoryErrorOccured = false; } @Override // TODO: Add all close(s) in finally statements protected WriterResults doInBackground(Trip... trips) { if (trips.length == 0) { return WriterResults.getFullFailureInstance(); //Should never be reached } // Set up our initial variables final Trip trip = trips[0]; final List<Receipt> receipts = mDB.getReceiptsTable().getBlocking(trip, false); final int len = receipts.size(); final WriterResults results = new WriterResults(); // Make our trip output directory exists in a good state File dir = trip.getDirectory(); if (!dir.exists()) { dir = mStorageManager.getFile(trip.getName()); if (!dir.exists()) { dir = mStorageManager.mkdir(trip.getName()); } } Logger.info(this, "Generating the following report types {}.", mOptions); if (mOptions.contains(EmailOptions.PDF_FULL)) { final Report pdfFullReport = new PdfBoxFullPdfReport(context, persistenceManager, flex); try { mFiles[EmailOptions.PDF_FULL.getIndex()] = pdfFullReport.generate(trip); } catch (ReportGenerationException e) { if (e.getCause() instanceof TooManyColumnsException) { results.didPDFFailTooManyColumns = true; } results.didPDFFailCompletely = true; } } if (mOptions.contains(EmailOptions.PDF_IMAGES_ONLY)) { final Report pdfimagesReport = new PdfBoxImagesOnlyReport(context, persistenceManager, flex); try { mFiles[EmailOptions.PDF_IMAGES_ONLY.getIndex()] = pdfimagesReport.generate(trip); } catch (ReportGenerationException e) { results.didPDFFailCompletely = true; } } if (mOptions.contains(EmailOptions.CSV)) { mStorageManager.delete(dir, dir.getName() + ".csv"); final List<Column<Receipt>> csvColumns = mDB.getCSVTable().get().blockingGet(); final CsvTableGenerator<Receipt> csvTableGenerator = new CsvTableGenerator<Receipt>(csvColumns, new LegacyReceiptFilter(mPreferenceManager), true, false); String data = csvTableGenerator.generate(receipts); if (mPreferenceManager.get(UserPreference.Distance.PrintDistanceTableInReports)) { final List<Distance> distances = new ArrayList<>(mDB.getDistanceTable().getBlocking(trip, false)); if (!distances.isEmpty()) { Collections.reverse(distances); // Reverse the list, so we print the most recent one first // CSVs cannot print special characters final ColumnDefinitions<Distance> distanceColumnDefinitions = new DistanceColumnDefinitions(context, mDB, mPreferenceManager, flex, true); final List<Column<Distance>> distanceColumns = distanceColumnDefinitions.getAllColumns(); data += "\n\n"; data += new CsvTableGenerator<>(distanceColumns, true, true).generate(distances); } } String filename = dir.getName() + ".csv"; File csvFile = new File(dir, filename); try { mFiles[EmailOptions.CSV.getIndex()] = csvFile; new CsvReportWriter(csvFile).write(data); } catch (IOException e) { Logger.error(this, "Failed to write the csv file", e); results.didCSVFailCompletely = true; } } if (mOptions.contains(EmailOptions.ZIP_IMAGES_STAMPED)) { mStorageManager.delete(dir, dir.getName() + ".zip"); dir = mStorageManager.mkdir(trip.getDirectory(), trip.getName()); for (int i = 0; i < len; i++) { if (!filterOutReceipt(mPreferenceManager, receipts.get(i)) && receipts.get(i).hasImage()) { try { Bitmap b = stampImage(trip, receipts.get(i), Bitmap.Config.ARGB_8888); if (b != null) { mStorageManager.writeBitmap(dir, b, receipts.get(i).getImage().getName(), CompressFormat.JPEG, 85); b.recycle(); b = null; } } catch (OutOfMemoryError e) { Logger.error(this, "Trying to recover from OOM", e); System.gc(); try { Bitmap b = stampImage(trip, receipts.get(i), Bitmap.Config.RGB_565); if (b != null) { mStorageManager.writeBitmap(dir, b, receipts.get(i).getImage().getName(), CompressFormat.JPEG, 85); b.recycle(); } } catch (OutOfMemoryError e2) { Logger.error(this, "Failed to recover from OOM", e2); results.didZIPFailCompletely = true; memoryErrorOccured = true; break; } } } } File zip = mStorageManager.zipBuffered(dir, 2048); mStorageManager.deleteRecursively(dir); mFiles[EmailOptions.ZIP_IMAGES_STAMPED.getIndex()] = zip; } return results; } /** * Applies a particular filter to determine whether or not this receipt should be * generated for this report * * @param preferences - User preferences * @param receipt - The particular receipt * @return true if if should be filtered out, false otherwise */ private boolean filterOutReceipt(UserPreferenceManager preferences, Receipt receipt) { if (preferences.get(UserPreference.Receipts.OnlyIncludeReimbursable) && !receipt.isReimbursable()) { return true; } else if (receipt.getPrice().getPriceAsFloat() < preferences.get(UserPreference.Receipts.MinimumReceiptPrice)) { return true; } else { return false; } } private static final float IMG_SCALE_FACTOR = 2.1f; private static final float HW_RATIO = 0.75f; private Bitmap stampImage(final Trip trip, final Receipt receipt, Bitmap.Config config) { if (!receipt.hasImage()) { return null; } Bitmap foreground = mStorageManager.getMutableMemoryEfficientBitmap(receipt.getImage()); if (foreground != null) { // It can be null if file not found // Size the image int foreWidth = foreground.getWidth(); int foreHeight = foreground.getHeight(); if (foreHeight > foreWidth) { foreWidth = (int) (foreHeight * HW_RATIO); } else { foreHeight = (int) (foreWidth / HW_RATIO); } // Set up the paddings int xPad = (int) (foreWidth / IMG_SCALE_FACTOR); int yPad = (int) (foreHeight / IMG_SCALE_FACTOR); // Set up an all white background for our canvas Bitmap background = Bitmap.createBitmap(foreWidth + xPad, foreHeight + yPad, config); Canvas canvas = new Canvas(background); canvas.drawARGB(0xFF, 0xFF, 0xFF, 0xFF); //This represents White color // Set up the paint Paint dither = new Paint(); dither.setDither(true); dither.setFilterBitmap(false); canvas.drawBitmap(foreground, (background.getWidth() - foreground.getWidth()) / 2, (background.getHeight() - foreground.getHeight()) / 2, dither); Paint brush = new Paint(); brush.setAntiAlias(true); brush.setTypeface(Typeface.SANS_SERIF); brush.setColor(Color.BLACK); brush.setStyle(Paint.Style.FILL); brush.setTextAlign(Align.LEFT); // Set up the number of items to draw int num = 5; if (mPreferenceManager.get(UserPreference.Receipts.IncludeTaxField)) { num++; } if (receipt.hasExtraEditText1()) { num++; } if (receipt.hasExtraEditText2()) { num++; } if (receipt.hasExtraEditText3()) { num++; } float spacing = getOptimalSpacing(num, yPad / 2, brush); float y = spacing * 4; canvas.drawText(trip.getName(), xPad / 2, y, brush); y += spacing; canvas.drawText(trip.getFormattedStartDate(context, persistenceManager.getPreferenceManager().get(UserPreference.General.DateSeparator)) + " -- " + trip.getFormattedEndDate(context, persistenceManager.getPreferenceManager().get(UserPreference.General.DateSeparator)), xPad / 2, y, brush); y += spacing; y = background.getHeight() - yPad / 2 + spacing * 2; canvas.drawText(flex.getString(context, R.string.RECEIPTMENU_FIELD_NAME) + ": " + receipt.getName(), xPad / 2, y, brush); y += spacing; canvas.drawText(flex.getString(context, R.string.RECEIPTMENU_FIELD_PRICE) + ": " + receipt.getPrice().getDecimalFormattedPrice() + " " + receipt.getPrice().getCurrencyCode(), xPad / 2, y, brush); y += spacing; if (mPreferenceManager.get(UserPreference.Receipts.IncludeTaxField)) { canvas.drawText(flex.getString(context, R.string.RECEIPTMENU_FIELD_TAX) + ": " + receipt.getTax().getDecimalFormattedPrice() + " " + receipt.getPrice().getCurrencyCode(), xPad / 2, y, brush); y += spacing; } canvas.drawText(flex.getString(context, R.string.RECEIPTMENU_FIELD_DATE) + ": " + receipt.getFormattedDate(context, persistenceManager.getPreferenceManager().get(UserPreference.General.DateSeparator)), xPad / 2, y, brush); y += spacing; canvas.drawText(flex.getString(context, R.string.RECEIPTMENU_FIELD_CATEGORY) + ": " + receipt.getCategory().getName(), xPad / 2, y, brush); y += spacing; canvas.drawText(flex.getString(context, R.string.RECEIPTMENU_FIELD_COMMENT) + ": " + receipt.getComment(), xPad / 2, y, brush); y += spacing; if (receipt.hasExtraEditText1()) { canvas.drawText(flex.getString(context, R.string.RECEIPTMENU_FIELD_EXTRA_EDITTEXT_1) + ": " + receipt.getExtraEditText1(), xPad / 2, y, brush); y += spacing; } if (receipt.hasExtraEditText2()) { canvas.drawText(flex.getString(context, R.string.RECEIPTMENU_FIELD_EXTRA_EDITTEXT_2) + ": " + receipt.getExtraEditText2(), xPad / 2, y, brush); y += spacing; } if (receipt.hasExtraEditText3()) { canvas.drawText(flex.getString(context, R.string.RECEIPTMENU_FIELD_EXTRA_EDITTEXT_3) + ": " + receipt.getExtraEditText3(), xPad / 2, y, brush); y += spacing; } // Clear out the dead data here foreground.recycle(); foreground = null; // And return return background; } else { return null; } } private float getOptimalSpacing(int count, int space, Paint brush) { float fontSize = 8f; //Seed brush.setTextSize(fontSize); while (space > (count + 2) * brush.getFontSpacing()) { brush.setTextSize(++fontSize); } brush.setTextSize(--fontSize); return brush.getFontSpacing(); } @Override protected void onPostExecute(WriterResults result) { ProgressDialog dialog = mProgressDialog.get(); //TODO: Check the other properties of result if necessary... if (result.didPDFFailCompletely) { if (dialog != null) { dialog.dismiss(); } if (result.didPDFFailTooManyColumns) { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.report_pdf_error_too_many_columns_title) .setMessage( mPreferenceManager.get(UserPreference.ReportOutput.PrintReceiptsTableInLandscape) ? context.getString(R.string.report_pdf_error_too_many_columns_message) : context.getString(R.string.report_pdf_error_too_many_columns_message_landscape) ) .setPositiveButton(R.string.report_pdf_error_go_to_settings, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { dialog.cancel(); navigationHandler.navigateToSettingsScrollToReportSection(); } }) .setNegativeButton(android.R.string.cancel, null) .show(); } else { Toast.makeText(context, R.string.report_pdf_generation_error, Toast.LENGTH_SHORT).show(); } } else { EmailAssistant.this.onAttachmentsCreated(mFiles); if (dialog != null) { dialog.dismiss(); dialog = null; } } } @Override protected void onProgressUpdate(Integer... values) { if (memoryErrorOccured) { memoryErrorOccured = false; Toast.makeText(context, "Error: Not enough memory to stamp the images. Try stopping some other apps and try again.", Toast.LENGTH_LONG).show(); } } } }