DO NOT MERGE. Prevent duplication of POP3 attachments
* For each attachment we add, check the DB for an existing attachment
with similar metadata (name, mime type, content id, etc.)
* Skip adding them if already held
* Unit tests
Originally fixed in 5b0a12c1991988 / CL I036f39c6
Fixes bug http://b/2084704
diff --git a/src/com/android/email/LegacyConversions.java b/src/com/android/email/LegacyConversions.java
index cd39aeb..4c1ccf8 100644
--- a/src/com/android/email/LegacyConversions.java
+++ b/src/com/android/email/LegacyConversions.java
@@ -38,6 +38,7 @@
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
+import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
@@ -50,6 +51,9 @@
public class LegacyConversions {
+ /** DO NOT CHECK IN "TRUE" */
+ private static final boolean DEBUG_ATTACHMENTS = false;
+
/**
* Values for HEADER_ANDROID_BODY_QUOTED_PART to tag body parts
*/
@@ -255,8 +259,14 @@
/**
* Add a single attachment part to the message
*
- * TODO: This will simply add to any existing attachments - could this ever happen? If so,
- * change it to find existing attachments and delete/merge them.
+ * This will skip adding attachments if they are already found in the attachments table.
+ * The heuristic for this will fail (false-positive) if two identical attachments are
+ * included in a single POP3 message.
+ * TODO: Fix that, by (elsewhere) simulating an mLocation value based on the attachments
+ * position within the list of multipart/mixed elements. This would make every POP3 attachment
+ * unique, and might also simplify the code (since we could just look at the positions, and
+ * ignore the filename, etc.)
+ *
* TODO: Take a closer look at encoding and deal with it if necessary.
*
* @param context a context for file operations
@@ -301,8 +311,44 @@
localAttachment.mLocation = partId;
localAttachment.mEncoding = "B"; // TODO - convert other known encodings
+ if (DEBUG_ATTACHMENTS) {
+ Log.d(Email.LOG_TAG, "Add attachment " + localAttachment);
+ }
+
+ // To prevent duplication - do we already have a matching attachment?
+ // The fields we'll check for equality are:
+ // mFileName, mMimeType, mContentId, mMessageKey, mLocation
+ // NOTE: This will false-positive if you attach the exact same file, twice, to a POP3
+ // message. We can live with that - you'll get one of the copies.
+ Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
+ Cursor cursor = context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION,
+ null, null, null);
+ boolean attachmentFoundInDb = false;
+ try {
+ while (cursor.moveToNext()) {
+ Attachment dbAttachment = new Attachment().restore(cursor);
+ // We test each of the fields here (instead of in SQL) because they may be
+ // null, or may be strings.
+ if (stringNotEqual(dbAttachment.mFileName, localAttachment.mFileName)) continue;
+ if (stringNotEqual(dbAttachment.mMimeType, localAttachment.mMimeType)) continue;
+ if (stringNotEqual(dbAttachment.mContentId, localAttachment.mContentId)) continue;
+ if (stringNotEqual(dbAttachment.mLocation, localAttachment.mLocation)) continue;
+ // We found a match, so use the existing attachment id, and stop looking/looping
+ attachmentFoundInDb = true;
+ localAttachment.mId = dbAttachment.mId;
+ if (DEBUG_ATTACHMENTS) {
+ Log.d(Email.LOG_TAG, "Skipped, found db attachment " + dbAttachment);
+ }
+ break;
+ }
+ } finally {
+ cursor.close();
+ }
+
// Save the attachment (so far) in order to obtain an id
- localAttachment.save(context);
+ if (!attachmentFoundInDb) {
+ localAttachment.save(context);
+ }
// If an attachment body was actually provided, we need to write the file now
saveAttachmentBody(context, part, localAttachment, localMessage.mAccountKey);
@@ -315,6 +361,17 @@
}
/**
+ * Helper for addOneAttachment that compares two strings, deals with nulls, and treats
+ * nulls and empty strings as equal.
+ */
+ /* package */ static boolean stringNotEqual(String a, String b) {
+ if (a == null && b == null) return false; // fast exit for two null strings
+ if (a == null) a = "";
+ if (b == null) b = "";
+ return !a.equals(b);
+ }
+
+ /**
* Save the body part of a single attachment, to a file in the attachments directory.
*/
public static void saveAttachmentBody(Context context, Part part, Attachment localAttachment,
diff --git a/src/com/android/email/provider/EmailContent.java b/src/com/android/email/provider/EmailContent.java
index 65f6775..62c7c53 100644
--- a/src/com/android/email/provider/EmailContent.java
+++ b/src/com/android/email/provider/EmailContent.java
@@ -1657,6 +1657,12 @@
return new EmailContent.Attachment[size];
}
};
+
+ @Override
+ public String toString() {
+ return "[" + mFileName + ", " + mMimeType + ", " + mSize + ", " + mContentId + ", "
+ + mContentUri + ", " + mMessageKey + ", " + mLocation + ", " + mEncoding + "]";
+ }
}
public interface MailboxColumns {
diff --git a/tests/src/com/android/email/LegacyConversionsTests.java b/tests/src/com/android/email/LegacyConversionsTests.java
index 91674cc..3fceb32 100644
--- a/tests/src/com/android/email/LegacyConversionsTests.java
+++ b/tests/src/com/android/email/LegacyConversionsTests.java
@@ -203,31 +203,7 @@
"local-message", accountId, mailboxId, false, true, mProviderContext);
// Prepare a legacy message with attachments
- Part attachment1Part = MessageTestUtils.bodyPart("image/gif", null);
- attachment1Part.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
- "image/gif;\n name=\"attachment1\"");
- attachment1Part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
- attachment1Part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
- "attachment;\n filename=\"attachment1\";\n size=100");
- attachment1Part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, "101");
-
- Part attachment2Part = MessageTestUtils.bodyPart("image/jpg", null);
- attachment2Part.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
- "image/jpg;\n name=\"attachment2\"");
- attachment2Part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
- attachment2Part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
- "attachment;\n filename=\"attachment2\";\n size=200");
- attachment2Part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, "102");
-
- final Message legacyMessage = new MessageBuilder()
- .setBody(new MultipartBuilder("multipart/mixed")
- .addBodyPart(MessageTestUtils.bodyPart("text/html", null))
- .addBodyPart(new MultipartBuilder("multipart/mixed")
- .addBodyPart((BodyPart)attachment1Part)
- .addBodyPart((BodyPart)attachment2Part)
- .buildBodyPart())
- .build())
- .build();
+ final Message legacyMessage = prepareLegacyMessageWithAttachments(2);
// Now, convert from legacy to provider and see what happens
ArrayList<Part> viewables = new ArrayList<Part>();
@@ -244,9 +220,9 @@
while (c.moveToNext()) {
Attachment attachment = Attachment.getContent(c, Attachment.class);
if ("101".equals(attachment.mLocation)) {
- checkAttachment("attachment1Part", attachment1Part, attachment);
+ checkAttachment("attachment1Part", attachments.get(0), attachment);
} else if ("102".equals(attachment.mLocation)) {
- checkAttachment("attachment2Part", attachment2Part, attachment);
+ checkAttachment("attachment2Part", attachments.get(1), attachment);
} else {
fail("Unexpected attachment with location " + attachment.mLocation);
}
@@ -257,6 +233,93 @@
}
/**
+ * Test that attachments aren't re-added in the DB. This supports the "partial download"
+ * nature of POP messages.
+ */
+ public void testAddDuplicateAttachments() throws MessagingException, IOException {
+ // Prepare a local message to add the attachments to
+ final long accountId = 1;
+ final long mailboxId = 1;
+ final EmailContent.Message localMessage = ProviderTestUtils.setupMessage(
+ "local-message", accountId, mailboxId, false, true, mProviderContext);
+
+ // Prepare a legacy message with attachments
+ Message legacyMessage = prepareLegacyMessageWithAttachments(2);
+
+ // Now, convert from legacy to provider and see what happens
+ ArrayList<Part> viewables = new ArrayList<Part>();
+ ArrayList<Part> attachments = new ArrayList<Part>();
+ MimeUtility.collectParts(legacyMessage, viewables, attachments);
+ LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments);
+
+ // Confirm two attachment objects created
+ Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
+ assertEquals(2, EmailContent.count(mProviderContext, uri, null, null));
+
+ // Now add the attachments again and confirm there are still only two
+ LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments);
+ assertEquals(2, EmailContent.count(mProviderContext, uri, null, null));
+
+ // Now add a 3rd & 4th attachment and make sure the total is 4, not 2 or 6
+ legacyMessage = prepareLegacyMessageWithAttachments(4);
+ viewables = new ArrayList<Part>();
+ attachments = new ArrayList<Part>();
+ MimeUtility.collectParts(legacyMessage, viewables, attachments);
+ LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments);
+ assertEquals(4, EmailContent.count(mProviderContext, uri, null, null));
+ }
+
+ /**
+ * Prepare a legacy message with 1+ attachments
+ */
+ private static Message prepareLegacyMessageWithAttachments(int numAttachments)
+ throws MessagingException {
+ // First, build one or more attachment parts
+ MultipartBuilder mpBuilder = new MultipartBuilder("multipart/mixed");
+ for (int i = 1; i <= numAttachments; ++i) {
+ BodyPart attachmentPart = MessageTestUtils.bodyPart("image/jpg", null);
+
+ // name=attachmentN size=N00 location=10N
+ attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
+ "image/jpg;\n name=\"attachment" + i + "\"");
+ attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+ attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
+ "attachment;\n filename=\"attachment2\";\n size=" + i + "00");
+ attachmentPart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, "10" + i);
+
+ mpBuilder.addBodyPart(attachmentPart);
+ }
+
+ // Now build a message with them
+ final Message legacyMessage = new MessageBuilder()
+ .setBody(new MultipartBuilder("multipart/mixed")
+ .addBodyPart(MessageTestUtils.bodyPart("text/html", null))
+ .addBodyPart(mpBuilder.buildBodyPart())
+ .build())
+ .build();
+
+ return legacyMessage;
+ }
+
+ /**
+ * Test the stringInequal helper
+ */
+ public void testStringInequal() {
+ // Pairs that are "equal"
+ assertFalse(LegacyConversions.stringNotEqual(null, null));
+ assertFalse(LegacyConversions.stringNotEqual(null, ""));
+ assertFalse(LegacyConversions.stringNotEqual("", null));
+ assertFalse(LegacyConversions.stringNotEqual("", ""));
+ assertFalse(LegacyConversions.stringNotEqual("string-equal", "string-equal"));
+ // Pairs that are "inequal"
+ assertTrue(LegacyConversions.stringNotEqual(null, "string-inequal"));
+ assertTrue(LegacyConversions.stringNotEqual("", "string-inequal"));
+ assertTrue(LegacyConversions.stringNotEqual("string-inequal", null));
+ assertTrue(LegacyConversions.stringNotEqual("string-inequal", ""));
+ assertTrue(LegacyConversions.stringNotEqual("string-inequal-a", "string-inequal-b"));
+ }
+
+ /**
* Compare attachment that was converted from Part (expected) to Provider Attachment (actual)
*
* TODO content URI should only be set if we also saved a file
@@ -345,7 +408,7 @@
assertEquals(tag, expect.isSet(Flag.FLAGGED), actual.mFlagFavorite);
}
- /**
+ /**
* Check equality of a pair of converted messages
*/
private void checkLegacyMessage(String tag, EmailContent.Message expect, Message actual)