From 7b5c73b43c01c4de0f4f66299a32346ed5a34844 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 12 Dec 2014 04:17:09 +0100 Subject: [PATCH 001/152] Add (failing) test for reconstructing a message from the database --- .../ReconstructMessageFromDatabaseTest.java | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 k9mail/src/androidTest/java/com/fsck/k9/mailstore/ReconstructMessageFromDatabaseTest.java diff --git a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/ReconstructMessageFromDatabaseTest.java b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/ReconstructMessageFromDatabaseTest.java new file mode 100644 index 000000000..a4b1aaaee --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/ReconstructMessageFromDatabaseTest.java @@ -0,0 +1,141 @@ +package com.fsck.k9.mailstore; + + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; + +import android.content.Context; +import android.test.ApplicationTestCase; +import android.test.RenamingDelegatingContext; + +import com.fsck.k9.Account; +import com.fsck.k9.K9; +import com.fsck.k9.mail.FetchProfile; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.internet.BinaryTempFileBody; +import com.fsck.k9.mail.internet.MimeMessage; + + +public class ReconstructMessageFromDatabaseTest extends ApplicationTestCase { + + public static final String MESSAGE_SOURCE = "From: from@example.com\r\n" + + "To: to@example.com\r\n" + + "Subject: Test Message \r\n" + + "Date: Thu, 13 Nov 2014 17:09:38 +0100\r\n" + + "Content-Type: multipart/mixed;\r\n" + + " boundary=\"----Boundary\"\r\n" + + "Content-Transfer-Encoding: 8bit\r\n" + + "MIME-Version: 1.0\r\n" + + "\r\n" + + "This is a multipart MIME message.\r\n" + + "------Boundary\r\n" + + "Content-Type: text/plain; charset=utf-8\r\n" + + "Content-Transfer-Encoding: 8bit\r\n" + + "\r\n" + + "Testing.\r\n" + + "This is a text body with some greek characters.\r\n" + + "αβγδεζηθ\r\n" + + "End of test.\r\n" + + "\r\n" + + "------Boundary\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Transfer-Encoding: base64\r\n" + + "\r\n" + + "VGhpcyBpcyBhIHRl\r\n" + + "c3QgbWVzc2FnZQ==\r\n" + + "\r\n" + + "------Boundary--\r\n" + + "Hi, I'm the epilogue"; + + private Account account; + + public ReconstructMessageFromDatabaseTest() { + super(K9.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + RenamingDelegatingContext context = new RenamingDelegatingContext(getContext(), "db-test-"); + setContext(context); + + BinaryTempFileBody.setTempDirectory(context.getCacheDir()); + + createApplication(); + + createDummyAccount(context); + } + + private void createDummyAccount(Context context) { + account = new DummyAccount(context); + } + + public void testThatByteIdenticalCopyOfMessageIsReconstructed() throws IOException, MessagingException { + + LocalFolder folder = createFolderInDatabase(); + + MimeMessage message = parseMessage(); + + saveMessageToDatabase(folder, message); + + LocalMessage localMessage = readMessageFromDatabase(folder, message); + + String reconstructedMessage = writeMessageToString(localMessage); + + assertEquals(MESSAGE_SOURCE, reconstructedMessage); + } + + protected MimeMessage parseMessage() throws IOException, MessagingException { + InputStream messageInputStream = new ByteArrayInputStream(MESSAGE_SOURCE.getBytes()); + try { + return new MimeMessage(messageInputStream, true); + } finally { + messageInputStream.close(); + } + } + + protected LocalFolder createFolderInDatabase() throws MessagingException { + LocalStore localStore = LocalStore.getInstance(account, getApplication()); + LocalFolder inbox = localStore.getFolder("INBOX"); + localStore.createFolders(Collections.singletonList(inbox), 10); + return inbox; + } + + protected void saveMessageToDatabase(LocalFolder folder, MimeMessage message) throws MessagingException { + folder.appendMessages(Collections.singletonList(message)); + } + + protected LocalMessage readMessageFromDatabase(LocalFolder folder, MimeMessage message) throws MessagingException { + LocalMessage localMessage = folder.getMessage(message.getUid()); + + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + fp.add(FetchProfile.Item.BODY); + folder.fetch(Collections.singletonList(localMessage), fp, null); + folder.close(); + + return localMessage; + } + + protected String writeMessageToString(LocalMessage localMessage) throws IOException, MessagingException { + ByteArrayOutputStream messageOutputStream = new ByteArrayOutputStream(); + try { + localMessage.writeTo(messageOutputStream); + } finally { + messageOutputStream.close(); + } + + return new String(messageOutputStream.toByteArray()); + } + + static class DummyAccount extends Account { + + protected DummyAccount(Context context) { + super(context); + } + } +} From abbad18283fcf743a247fe797638ba6136ff1c20 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 5 Jan 2015 00:33:13 +0100 Subject: [PATCH 002/152] Code style fixes --- .../com/fsck/k9/mailstore/LocalFolder.java | 123 ++++++++---------- 1 file changed, 52 insertions(+), 71 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index cc353a06e..37f969108 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -1,5 +1,6 @@ package com.fsck.k9.mailstore; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -17,10 +18,6 @@ import java.util.Set; import java.util.UUID; import java.util.regex.Pattern; -import com.fsck.k9.mail.internet.MimeMessageHelper; -import org.apache.commons.io.IOUtils; -import org.apache.james.mime4j.util.MimeUtil; - import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; @@ -30,10 +27,9 @@ import android.net.Uri; import android.util.Log; import com.fsck.k9.Account; -import com.fsck.k9.K9; import com.fsck.k9.Account.MessageFormat; +import com.fsck.k9.K9; import com.fsck.k9.activity.Search; -import com.fsck.k9.mail.MessageRetrievalListener; import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Address; @@ -43,18 +39,22 @@ import com.fsck.k9.mail.FetchProfile; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.Message.RecipientType; +import com.fsck.k9.mail.MessageRetrievalListener; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; -import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.internet.MimeMessageHelper; import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.WrappedException; import com.fsck.k9.provider.AttachmentProvider; +import org.apache.commons.io.IOUtils; +import org.apache.james.mime4j.util.MimeUtil; public class LocalFolder extends Folder implements Serializable { @@ -175,7 +175,7 @@ public class LocalFolder extends Folder implements Serializable { // does a DB update on setLastChecked super.setLastChecked(cursor.getLong(LocalStore.FOLDER_LAST_CHECKED_INDEX)); super.setLastPush(cursor.getLong(LocalStore.FOLDER_LAST_PUSHED_INDEX)); - mInTopGroup = (cursor.getInt(LocalStore.FOLDER_TOP_GROUP_INDEX)) == 1 ? true : false; + mInTopGroup = (cursor.getInt(LocalStore.FOLDER_TOP_GROUP_INDEX)) == 1 ? true : false; mIntegrate = (cursor.getInt(LocalStore.FOLDER_INTEGRATE_INDEX) == 1) ? true : false; String noClass = FolderClass.NO_CLASS.toString(); String displayClass = cursor.getString(LocalStore.FOLDER_DISPLAY_CLASS_INDEX); @@ -210,10 +210,8 @@ public class LocalFolder extends Folder implements Serializable { public Boolean doDbWork(final SQLiteDatabase db) throws WrappedException { Cursor cursor = null; try { - cursor = db.rawQuery("SELECT id FROM folders " - + "where folders.name = ?", new String[] { LocalFolder. - this.getName() - }); + cursor = db.rawQuery("SELECT id FROM folders where folders.name = ?", + new String[] { LocalFolder.this.getName() }); if (cursor.moveToFirst()) { int folderId = cursor.getInt(0); return (folderId > 0); @@ -271,10 +269,10 @@ public class LocalFolder extends Folder implements Serializable { } Cursor cursor = null; try { - cursor = db.rawQuery("SELECT COUNT(id) FROM messages WHERE (empty IS NULL OR empty != 1) AND deleted = 0 and folder_id = ?", - new String[] { - Long.toString(mFolderId) - }); + cursor = db.rawQuery( + "SELECT COUNT(id) FROM messages " + + "WHERE (empty IS NULL OR empty != 1) AND deleted = 0 and folder_id = ?", + new String[] { Long.toString(mFolderId) }); cursor.moveToFirst(); return cursor.getInt(0); //messagecount } finally { @@ -283,7 +281,7 @@ public class LocalFolder extends Folder implements Serializable { } }); } catch (WrappedException e) { - throw(MessagingException) e.getCause(); + throw (MessagingException) e.getCause(); } } @@ -403,6 +401,7 @@ public class LocalFolder extends Folder implements Serializable { public void setStatus(final String status) throws MessagingException { updateFolderColumn("status", status); } + public void setPushState(final String pushState) throws MessagingException { mPushState = pushState; updateFolderColumn("push_state", pushState); @@ -465,7 +464,6 @@ public class LocalFolder extends Folder implements Serializable { public void setDisplayClass(FolderClass displayClass) throws MessagingException { mDisplayClass = displayClass; updateFolderColumn("display_class", mDisplayClass.name()); - } public void setSyncClass(FolderClass syncClass) throws MessagingException { @@ -503,7 +501,6 @@ public class LocalFolder extends Folder implements Serializable { private String getPrefId() throws MessagingException { open(OPEN_MODE_RW); return getPrefId(mName); - } public void delete() throws MessagingException { @@ -606,7 +603,6 @@ public class LocalFolder extends Folder implements Serializable { } prefHolder.inTopGroup = preferences.getBoolean(id + ".inTopGroup", prefHolder.inTopGroup); prefHolder.integrate = preferences.getBoolean(id + ".integrate", prefHolder.integrate); - } @Override @@ -847,13 +843,12 @@ public class LocalFolder extends Folder implements Serializable { Long id = message.getId(); ids.add(Long.toString(id)); popMessages.put(id, message); - } cursor = db.rawQuery( - "SELECT message_id, name, value FROM headers " + "WHERE message_id in ( " + questions + ") ORDER BY id ASC", - ids.toArray(LocalStore.EMPTY_STRING_ARRAY)); - + "SELECT message_id, name, value FROM headers " + + "WHERE message_id in ( " + questions + ") ORDER BY id ASC", + ids.toArray(LocalStore.EMPTY_STRING_ARRAY)); while (cursor.moveToNext()) { Long id = cursor.getLong(0); @@ -881,11 +876,8 @@ public class LocalFolder extends Folder implements Serializable { try { cursor = db.rawQuery( - "SELECT uid FROM messages " + - "WHERE id = ? AND folder_id = ?", - new String[] { - Long.toString(id), Long.toString(mFolderId) - }); + "SELECT uid FROM messages WHERE id = ? AND folder_id = ?", + new String[] { Long.toString(id), Long.toString(mFolderId) }); if (!cursor.moveToNext()) { return null; } @@ -916,14 +908,13 @@ public class LocalFolder extends Folder implements Serializable { try { cursor = db.rawQuery( - "SELECT " + - LocalStore.GET_MESSAGES_COLS + - "FROM messages " + - "LEFT JOIN threads ON (threads.message_id = messages.id) " + - "WHERE uid = ? AND folder_id = ?", - new String[] { - message.getUid(), Long.toString(mFolderId) - }); + "SELECT " + + LocalStore.GET_MESSAGES_COLS + + "FROM messages " + + "LEFT JOIN threads ON (threads.message_id = messages.id) " + + "WHERE uid = ? AND folder_id = ?", + new String[] { message.getUid(), Long.toString(mFolderId) }); + if (!cursor.moveToNext()) { return null; } @@ -955,17 +946,14 @@ public class LocalFolder extends Folder implements Serializable { public List doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { try { open(OPEN_MODE_RW); - return LocalFolder.this.localStore.getMessages( - listener, - LocalFolder.this, - "SELECT " + LocalStore.GET_MESSAGES_COLS + - "FROM messages " + - "LEFT JOIN threads ON (threads.message_id = messages.id) " + - "WHERE (empty IS NULL OR empty != 1) AND " + - (includeDeleted ? "" : "deleted = 0 AND ") + - "folder_id = ? ORDER BY date DESC", - new String[] { Long.toString(mFolderId) } - ); + return LocalFolder.this.localStore.getMessages(listener, LocalFolder.this, + "SELECT " + LocalStore.GET_MESSAGES_COLS + + "FROM messages " + + "LEFT JOIN threads ON (threads.message_id = messages.id) " + + "WHERE (empty IS NULL OR empty != 1) AND " + + (includeDeleted ? "" : "deleted = 0 AND ") + + "folder_id = ? ORDER BY date DESC", + new String[] { Long.toString(mFolderId) }); } catch (MessagingException e) { throw new WrappedException(e); } @@ -1485,14 +1473,14 @@ public class LocalFolder extends Folder implements Serializable { deleteHeaders(id); for (String name : message.getHeaderNames()) { - String[] values = message.getHeader(name); - for (String value : values) { - ContentValues cv = new ContentValues(); - cv.put("message_id", id); - cv.put("name", name); - cv.put("value", value); - db.insert("headers", "name", cv); - } + String[] values = message.getHeader(name); + for (String value : values) { + ContentValues cv = new ContentValues(); + cv.put("message_id", id); + cv.put("name", name); + cv.put("value", value); + db.insert("headers", "name", cv); + } } // Remember that all headers for this message have been saved, so it is @@ -1502,8 +1490,7 @@ public class LocalFolder extends Folder implements Serializable { appendedFlags.add(Flag.X_GOT_ALL_HEADERS); db.execSQL("UPDATE messages " + "SET flags = ? " + " WHERE id = ?", - new Object[] - { LocalFolder.this.localStore.serializeFlags(appendedFlags), id }); + new Object[] { LocalFolder.this.localStore.serializeFlags(appendedFlags), id }); return null; } @@ -1514,8 +1501,7 @@ public class LocalFolder extends Folder implements Serializable { this.localStore.database.execute(false, new DbCallback() { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - db.execSQL("DELETE FROM headers WHERE message_id = ?", new Object[] - { id }); + db.execSQL("DELETE FROM headers WHERE message_id = ?", new Object[] { id }); return null; } }); @@ -1776,17 +1762,12 @@ public class LocalFolder extends Folder implements Serializable { public void clearMessagesOlderThan(long cutoff) throws MessagingException { open(OPEN_MODE_RO); - List messages = this.localStore.getMessages( - null, - this, - "SELECT " + LocalStore.GET_MESSAGES_COLS + - "FROM messages " + - "LEFT JOIN threads ON (threads.message_id = messages.id) " + - "WHERE (empty IS NULL OR empty != 1) AND " + - "(folder_id = ? and date < ?)", - new String[] { - Long.toString(mFolderId), Long.toString(cutoff) - }); + List messages = this.localStore.getMessages(null, this, + "SELECT " + LocalStore.GET_MESSAGES_COLS + + "FROM messages " + + "LEFT JOIN threads ON (threads.message_id = messages.id) " + + "WHERE (empty IS NULL OR empty != 1) AND (folder_id = ? and date < ?)", + new String[] { Long.toString(mFolderId), Long.toString(cutoff) }); for (Message message : messages) { message.destroy(); From d7085a2f07df9719e1fdbdfc0a450b487af09d0b Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 5 Jan 2015 00:57:25 +0100 Subject: [PATCH 003/152] Properly decode the body in MessageExtractor.getTextFromPart() --- .../main/java/com/fsck/k9/mail/internet/MessageExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java index ba0bfa42f..809be651b 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java @@ -84,7 +84,7 @@ public class MessageExtractor { * Now we read the part into a buffer for further processing. Because * the stream is now wrapped we'll remove any transfer encoding at this point. */ - InputStream in = part.getBody().getInputStream(); + InputStream in = MimeUtility.decodeBody(body); try { String text = CharsetSupport.readToString(in, charset); From 3eb25a011f220402006993dc63f855defaebb400 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 5 Jan 2015 01:07:04 +0100 Subject: [PATCH 004/152] Don't automatically create Message-ID when none is found --- .../main/java/com/fsck/k9/mail/internet/MimeMessage.java | 9 ++++----- .../main/java/com/fsck/k9/activity/MessageCompose.java | 1 + .../src/main/java/com/fsck/k9/mailstore/LocalFolder.java | 6 ++++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java index 632018c29..981e441a1 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java @@ -308,13 +308,10 @@ public class MimeMessage extends Message { if (mMessageId == null) { mMessageId = getFirstHeader("Message-ID"); } - if (mMessageId == null) { // even after checking the header - setMessageId(generateMessageId()); - } return mMessageId; } - private String generateMessageId() { + public void generateMessageId() throws MessagingException { String hostname = null; if (mFrom != null && mFrom.length >= 1) { @@ -330,7 +327,9 @@ public class MimeMessage extends Message { } /* We use upper case here to match Apple Mail Message-ID format (for privacy) */ - return "<" + UUID.randomUUID().toString().toUpperCase(Locale.US) + "@" + hostname + ">"; + String messageId = "<" + UUID.randomUUID().toString().toUpperCase(Locale.US) + "@" + hostname + ">"; + + setMessageId(messageId); } public void setMessageId(String messageId) throws MessagingException { diff --git a/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java b/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java index 0129b82e1..31b87dfd8 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java @@ -1338,6 +1338,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, private MimeMessage createMessage(boolean isDraft) throws MessagingException { MimeMessage message = new MimeMessage(); message.addSentDate(new Date(), K9.hideTimeZone()); + message.generateMessageId(); Address from = new Address(mIdentity.getEmail(), mIdentity.getName()); message.setFrom(from); message.setRecipients(RecipientType.TO, getAddresses(mToView)); diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 37f969108..9dfb0f51b 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -1179,6 +1179,10 @@ public class LocalFolder extends Folder implements Serializable { } private ThreadInfo getThreadInfo(SQLiteDatabase db, String messageId, boolean onlyEmpty) { + if (messageId == null) { + return null; + } + String sql = "SELECT t.id, t.message_id, t.root, t.parent " + "FROM messages m " + "LEFT JOIN threads t ON (t.message_id = m.id) " + @@ -1393,8 +1397,6 @@ public class LocalFolder extends Folder implements Serializable { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { try { - message.buildMimeRepresentation(); - ViewableContainer container = LocalMessageExtractor.extractTextAndAttachments(LocalFolder.this.localStore.context, message); From 523ebd0f2afeba538924a5836bc242429c8202ed Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 5 Jan 2015 02:24:56 +0100 Subject: [PATCH 005/152] Remove 'dirty' check for LocalMessage --- .../com/fsck/k9/mailstore/LocalMessage.java | 32 +++---------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java index 48ed576d2..a69fa751f 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java @@ -37,7 +37,6 @@ public class LocalMessage extends MimeMessage { private String mPreview = ""; private boolean mHeadersLoaded = false; - private boolean mMessageDirty = false; private long mThreadId; private long mRootId; @@ -142,30 +141,13 @@ public class LocalMessage extends MimeMessage { @Override public void writeTo(OutputStream out) throws IOException, MessagingException { - if (mMessageDirty) buildMimeRepresentation(); + if (!mHeadersLoaded) { + loadHeaders(); + } + super.writeTo(out); } - void buildMimeRepresentation() throws MessagingException { - if (!mMessageDirty) { - return; - } - - super.setSubject(mSubject); - if (this.mFrom != null && this.mFrom.length > 0) { - super.setFrom(this.mFrom[0]); - } - - super.setReplyTo(mReplyTo); - super.setSentDate(this.getSentDate(), K9.hideTimeZone()); - super.setRecipients(RecipientType.TO, mTo); - super.setRecipients(RecipientType.CC, mCc); - super.setRecipients(RecipientType.BCC, mBcc); - if (mMessageId != null) super.setMessageId(mMessageId); - - mMessageDirty = false; - } - @Override public String getPreview() { return mPreview; @@ -180,14 +162,12 @@ public class LocalMessage extends MimeMessage { @Override public void setSubject(String subject) throws MessagingException { mSubject = subject; - mMessageDirty = true; } @Override public void setMessageId(String messageId) { mMessageId = messageId; - mMessageDirty = true; } @Override @@ -208,7 +188,6 @@ public class LocalMessage extends MimeMessage { @Override public void setFrom(Address from) throws MessagingException { this.mFrom = new Address[] { from }; - mMessageDirty = true; } @@ -219,7 +198,6 @@ public class LocalMessage extends MimeMessage { } else { mReplyTo = replyTo; } - mMessageDirty = true; } @@ -250,7 +228,6 @@ public class LocalMessage extends MimeMessage { } else { throw new MessagingException("Unrecognized recipient type."); } - mMessageDirty = true; } public void setFlagInternal(Flag flag, boolean set) throws MessagingException { @@ -557,7 +534,6 @@ public class LocalMessage extends MimeMessage { message.mSubject = mSubject; message.mPreview = mPreview; message.mHeadersLoaded = mHeadersLoaded; - message.mMessageDirty = mMessageDirty; return message; } From d7edb0ed4f4d05a34506ddad2f91436385d9e549 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 6 Jan 2015 01:37:59 +0100 Subject: [PATCH 006/152] Minimal version that reconstructs original message from the database This change breaks all kinds of things, e.g. - deleting messages - updating messages - downloading attachments - deleting attachments - searching in message bodies --- .../main/java/com/fsck/k9/mail/Multipart.java | 7 +- .../src/main/java/com/fsck/k9/mail/Part.java | 2 + .../fsck/k9/mail/internet/MimeBodyPart.java | 6 +- .../fsck/k9/mail/internet/MimeMessage.java | 10 +- .../k9/mail/internet/MimeMessageHelper.java | 7 +- .../fsck/k9/mail/internet/MimeMultipart.java | 69 +- .../com/fsck/k9/activity/MessageCompose.java | 3 +- .../fsck/k9/mailstore/BinaryMemoryBody.java | 42 + .../com/fsck/k9/mailstore/LocalFolder.java | 745 ++++++++++-------- .../com/fsck/k9/mailstore/LocalMessage.java | 35 +- .../com/fsck/k9/mailstore/LocalStore.java | 4 +- .../k9/mailstore/StoreSchemaDefinition.java | 37 +- 12 files changed, 553 insertions(+), 414 deletions(-) create mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Multipart.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Multipart.java index 5f82e062d..b89b69182 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Multipart.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Multipart.java @@ -28,7 +28,9 @@ public abstract class Multipart implements CompositeBody { return Collections.unmodifiableList(mParts); } - public abstract String getContentType(); + public abstract String getMimeType(); + + public abstract String getBoundary(); public int getCount() { return mParts.size(); @@ -64,4 +66,7 @@ public abstract class Multipart implements CompositeBody { ((TextBody)body).setCharset(charset); } } + + public abstract byte[] getPreamble(); + public abstract byte[] getEpilogue(); } diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java index 1d0274f32..15e371392 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java @@ -31,6 +31,8 @@ public interface Part { void writeTo(OutputStream out) throws IOException, MessagingException; + void writeHeaderTo(OutputStream out) throws IOException, MessagingException; + /** * Called just prior to transmission, once the type of transport is known to * be 7bit. diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java index 32e3e2654..801f1cbde 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java @@ -5,7 +5,6 @@ import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.CompositeBody; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.Multipart; import java.io.BufferedWriter; import java.io.IOException; @@ -135,6 +134,11 @@ public class MimeBodyPart extends BodyPart { } } + @Override + public void writeHeaderTo(OutputStream out) throws IOException, MessagingException { + mHeader.writeTo(out); + } + @Override public void setUsing7bitTransport() throws MessagingException { String type = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java index 981e441a1..e7909c105 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java @@ -443,6 +443,11 @@ public class MimeMessage extends Message { } } + @Override + public void writeHeaderTo(OutputStream out) throws IOException, MessagingException { + mHeader.writeTo(out); + } + @Override public InputStream getInputStream() throws MessagingException { return null; @@ -518,7 +523,10 @@ public class MimeMessage extends Message { Part e = (Part)stack.peek(); try { - MimeMultipart multiPart = new MimeMultipart(e.getContentType()); + String contentType = e.getContentType(); + String mimeType = MimeUtility.getHeaderParameter(contentType, null); + String boundary = MimeUtility.getHeaderParameter(contentType, "boundary"); + MimeMultipart multiPart = new MimeMultipart(mimeType, boundary); e.setBody(multiPart); stack.addFirst(multiPart); } catch (MessagingException me) { diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java index bc1695607..6cb1bd64f 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java @@ -23,9 +23,10 @@ public class MimeMessageHelper { if (body instanceof Multipart) { Multipart multipart = ((Multipart) body); multipart.setParent(part); - String type = multipart.getContentType(); - part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, type); - if ("multipart/signed".equalsIgnoreCase(type)) { + String mimeType = multipart.getMimeType(); + String contentType = String.format("%s; boundary=\"%s\"", mimeType, multipart.getBoundary()); + part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); + if ("multipart/signed".equalsIgnoreCase(mimeType)) { setEncoding(part, MimeUtil.ENC_7BIT); } else { setEncoding(part, MimeUtil.ENC_8BIT); diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMultipart.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMultipart.java index d6ce4377a..ae4c52016 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMultipart.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMultipart.java @@ -10,30 +10,26 @@ import java.util.Locale; import java.util.Random; public class MimeMultipart extends Multipart { - private byte[] mPreamble; - private byte[] mEpilogue; - - private String mContentType; - - private final String mBoundary; + private String mimeType; + private byte[] preamble; + private byte[] epilogue; + private final String boundary; public MimeMultipart() throws MessagingException { - mBoundary = generateBoundary(); + boundary = generateBoundary(); setSubType("mixed"); } - public MimeMultipart(String contentType) throws MessagingException { - this.mContentType = contentType; - try { - mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary"); - if (mBoundary == null) { - throw new MessagingException("MultiPart does not contain boundary: " + contentType); - } - } catch (Exception e) { - throw new MessagingException( - "Invalid MultiPart Content-Type; must contain subtype and boundary. (" - + contentType + ")", e); + public MimeMultipart(String mimeType, String boundary) throws MessagingException { + if (mimeType == null) { + throw new IllegalArgumentException("mimeType can't be null"); } + if (boundary == null) { + throw new IllegalArgumentException("boundary can't be null"); + } + + this.mimeType = mimeType; + this.boundary = boundary; } public String generateBoundary() { @@ -46,40 +42,53 @@ public class MimeMultipart extends Multipart { return sb.toString().toUpperCase(Locale.US); } + @Override + public String getBoundary() { + return boundary; + } + + public byte[] getPreamble() { + return preamble; + } + public void setPreamble(byte[] preamble) { - this.mPreamble = preamble; + this.preamble = preamble; + } + + public byte[] getEpilogue() { + return epilogue; } public void setEpilogue(byte[] epilogue) { - mEpilogue = epilogue; + this.epilogue = epilogue; } @Override - public String getContentType() { - return mContentType; + public String getMimeType() { + return mimeType; } public void setSubType(String subType) { - mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary); + mimeType = "multipart/" + subType; } @Override public void writeTo(OutputStream out) throws IOException, MessagingException { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); - if (mPreamble != null) { - out.write(mPreamble); + if (preamble != null) { + out.write(preamble); writer.write("\r\n"); } if (getBodyParts().isEmpty()) { writer.write("--"); - writer.write(mBoundary); + writer.write(boundary); writer.write("\r\n"); } else { for (BodyPart bodyPart : getBodyParts()) { writer.write("--"); - writer.write(mBoundary); + writer.write(boundary); writer.write("\r\n"); writer.flush(); bodyPart.writeTo(out); @@ -88,11 +97,11 @@ public class MimeMultipart extends Multipart { } writer.write("--"); - writer.write(mBoundary); + writer.write(boundary); writer.write("--\r\n"); writer.flush(); - if (mEpilogue != null) { - out.write(mEpilogue); + if (epilogue != null) { + out.write(epilogue); } } diff --git a/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java b/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java index 31b87dfd8..8fff3fbb0 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java @@ -1338,7 +1338,6 @@ public class MessageCompose extends K9Activity implements OnClickListener, private MimeMessage createMessage(boolean isDraft) throws MessagingException { MimeMessage message = new MimeMessage(); message.addSentDate(new Date(), K9.hideTimeZone()); - message.generateMessageId(); Address from = new Address(mIdentity.getEmail(), mIdentity.getName()); message.setFrom(from); message.setRecipients(RecipientType.TO, getAddresses(mToView)); @@ -1426,6 +1425,8 @@ public class MessageCompose extends K9Activity implements OnClickListener, message.addHeader(K9.IDENTITY_HEADER, buildIdentityHeader(body, bodyPlain)); } + message.generateMessageId(); + return message; } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java b/k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java new file mode 100644 index 000000000..46b9e8339 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java @@ -0,0 +1,42 @@ +package com.fsck.k9.mailstore; + + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.internet.RawDataBody; + + +public class BinaryMemoryBody implements Body, RawDataBody { + private final byte[] data; + private final String encoding; + + public BinaryMemoryBody(byte[] data, String encoding) { + this.data = data; + this.encoding = encoding; + } + + @Override + public String getEncoding() { + return encoding; + } + + @Override + public InputStream getInputStream() throws MessagingException { + return new ByteArrayInputStream(data); + } + + @Override + public void setEncoding(String encoding) throws UnavailableStorageException, MessagingException { + throw new RuntimeException("nope"); //FIXME + } + + @Override + public void writeTo(OutputStream out) throws IOException, MessagingException { + out.write(data); + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 9dfb0f51b..38688de64 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -1,6 +1,8 @@ package com.fsck.k9.mailstore; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -15,6 +17,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.Stack; import java.util.UUID; import java.util.regex.Pattern; @@ -27,7 +30,6 @@ import android.net.Uri; import android.util.Log; import com.fsck.k9.Account; -import com.fsck.k9.Account.MessageFormat; import com.fsck.k9.K9; import com.fsck.k9.activity.Search; import com.fsck.k9.helper.HtmlConverter; @@ -42,6 +44,7 @@ import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.MessageRetrievalListener; import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeHeader; @@ -49,18 +52,23 @@ import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMessageHelper; import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; -import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.WrappedException; import com.fsck.k9.provider.AttachmentProvider; import org.apache.commons.io.IOUtils; +import org.apache.james.mime4j.MimeException; +import org.apache.james.mime4j.parser.ContentHandler; +import org.apache.james.mime4j.parser.MimeStreamParser; +import org.apache.james.mime4j.stream.BodyDescriptor; +import org.apache.james.mime4j.stream.Field; +import org.apache.james.mime4j.stream.MimeConfig; import org.apache.james.mime4j.util.MimeUtil; public class LocalFolder extends Folder implements Serializable { private static final long serialVersionUID = -1973296520918624767L; - + private final LocalStore localStore; private String mName = null; @@ -616,182 +624,9 @@ public class LocalFolder extends Folder implements Serializable { open(OPEN_MODE_RW); if (fp.contains(FetchProfile.Item.BODY)) { for (Message message : messages) { - LocalMessage localMessage = (LocalMessage)message; - Cursor cursor = null; - MimeMultipart mp = new MimeMultipart(); - mp.setSubType("mixed"); - try { - cursor = db.rawQuery("SELECT html_content, text_content, mime_type FROM messages " - + "WHERE id = ?", - new String[] { Long.toString(localMessage.getId()) }); - cursor.moveToNext(); - String htmlContent = cursor.getString(0); - String textContent = cursor.getString(1); - String mimeType = cursor.getString(2); - if (mimeType != null && mimeType.toLowerCase(Locale.US).startsWith("multipart/")) { - // If this is a multipart message, preserve both text - // and html parts, as well as the subtype. - mp.setSubType(mimeType.toLowerCase(Locale.US).replaceFirst("^multipart/", "")); - if (textContent != null) { - LocalTextBody body = new LocalTextBody(textContent, htmlContent); - MimeBodyPart bp = new MimeBodyPart(body, "text/plain"); - mp.addBodyPart(bp); - } + LocalMessage localMessage = (LocalMessage) message; - if (getAccount().getMessageFormat() != MessageFormat.TEXT) { - if (htmlContent != null) { - TextBody body = new TextBody(htmlContent); - MimeBodyPart bp = new MimeBodyPart(body, "text/html"); - mp.addBodyPart(bp); - } - - // If we have both text and html content and our MIME type - // isn't multipart/alternative, then corral them into a new - // multipart/alternative part and put that into the parent. - // If it turns out that this is the only part in the parent - // MimeMultipart, it'll get fixed below before we attach to - // the message. - if (textContent != null && htmlContent != null && !mimeType.equalsIgnoreCase("multipart/alternative")) { - MimeMultipart alternativeParts = mp; - alternativeParts.setSubType("alternative"); - mp = new MimeMultipart(); - mp.addBodyPart(new MimeBodyPart(alternativeParts)); - } - } - } else if (mimeType != null && mimeType.equalsIgnoreCase("text/plain")) { - // If it's text, add only the plain part. The MIME - // container will drop away below. - if (textContent != null) { - LocalTextBody body = new LocalTextBody(textContent, htmlContent); - MimeBodyPart bp = new MimeBodyPart(body, "text/plain"); - mp.addBodyPart(bp); - } - } else if (mimeType != null && mimeType.equalsIgnoreCase("text/html")) { - // If it's html, add only the html part. The MIME - // container will drop away below. - if (htmlContent != null) { - TextBody body = new TextBody(htmlContent); - MimeBodyPart bp = new MimeBodyPart(body, "text/html"); - mp.addBodyPart(bp); - } - } else { - // MIME type not set. Grab whatever part we can get, - // with Text taking precedence. This preserves pre-HTML - // composition behaviour. - if (textContent != null) { - LocalTextBody body = new LocalTextBody(textContent, htmlContent); - MimeBodyPart bp = new MimeBodyPart(body, "text/plain"); - mp.addBodyPart(bp); - } else if (htmlContent != null) { - TextBody body = new TextBody(htmlContent); - MimeBodyPart bp = new MimeBodyPart(body, "text/html"); - mp.addBodyPart(bp); - } - } - - } catch (Exception e) { - Log.e(K9.LOG_TAG, "Exception fetching message:", e); - } finally { - Utility.closeQuietly(cursor); - } - - try { - cursor = db.query( - "attachments", - new String[] { - "id", - "size", - "name", - "mime_type", - "store_data", - "content_uri", - "content_id", - "content_disposition" - }, - "message_id = ?", - new String[] { Long.toString(localMessage.getId()) }, - null, - null, - null); - - while (cursor.moveToNext()) { - long id = cursor.getLong(0); - int size = cursor.getInt(1); - String name = cursor.getString(2); - String type = cursor.getString(3); - String storeData = cursor.getString(4); - String contentUri = cursor.getString(5); - String contentId = cursor.getString(6); - String contentDisposition = cursor.getString(7); - String encoding = MimeUtility.getEncodingforType(type); - Body body = null; - - if (contentDisposition == null) { - contentDisposition = "attachment"; - } - - if (contentUri != null) { - if (MimeUtil.isMessage(type)) { - body = new LocalAttachmentMessageBody( - Uri.parse(contentUri), - LocalFolder.this.localStore.context); - } else { - body = new LocalAttachmentBody( - Uri.parse(contentUri), - LocalFolder.this.localStore.context); - } - } - - MimeBodyPart bp = new LocalAttachmentBodyPart(body, id); - bp.setEncoding(encoding); - if (name != null) { - bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE, - String.format("%s;\r\n name=\"%s\"", - type, - name)); - bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, - String.format(Locale.US, "%s;\r\n filename=\"%s\";\r\n size=%d", - contentDisposition, - name, // TODO: Should use encoded word defined in RFC 2231. - size)); - } else { - bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE, type); - bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, - String.format(Locale.US, "%s;\r\n size=%d", - contentDisposition, - size)); - } - - bp.setHeader(MimeHeader.HEADER_CONTENT_ID, contentId); - /* - * HEADER_ANDROID_ATTACHMENT_STORE_DATA is a custom header we add to that - * we can later pull the attachment from the remote store if necessary. - */ - bp.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, storeData); - - mp.addBodyPart(bp); - } - } finally { - Utility.closeQuietly(cursor); - } - - if (mp.getCount() == 0) { - // If we have no body, remove the container and create a - // dummy plain text body. This check helps prevents us from - // triggering T_MIME_NO_TEXT and T_TVD_MIME_NO_HEADERS - // SpamAssassin rules. - localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "text/plain"); - MimeMessageHelper.setBody(localMessage, new TextBody("")); - } else if (mp.getCount() == 1 && - !(mp.getBodyPart(0) instanceof LocalAttachmentBodyPart)) { - // If we have only one part, drop the MimeMultipart container. - BodyPart part = mp.getBodyPart(0); - localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, part.getContentType()); - MimeMessageHelper.setBody(localMessage, part.getBody()); - } else { - // Otherwise, attach the MimeMultipart to the message. - MimeMessageHelper.setBody(localMessage, mp); - } + loadMessageParts(db, localMessage); } } } catch (MessagingException e) { @@ -801,7 +636,156 @@ public class LocalFolder extends Folder implements Serializable { } }); } catch (WrappedException e) { - throw(MessagingException) e.getCause(); + throw (MessagingException) e.getCause(); + } + } + + private void loadMessageParts(SQLiteDatabase db, LocalMessage message) throws MessagingException { + Map partById = new HashMap(); + + String[] columns = { + "id", // 0 + "type", // 1 + "parent", // 2 + "mime_type", // 3 + "decoded_body_size", // 4 + "display_name", // 5 + "header", // 6 + "encoding", // 7 + "charset", // 8 + "data_location", // 9 + "data", // 10 + "preamble", // 11 + "epilogue", // 12 + "boundary", // 13 + "content_id", // 14 + "server_extra", // 15 + }; + Cursor cursor = db.query("message_parts", columns, "root = ?", + new String[] { String.valueOf(message.getMessagePartId()) }, null, null, "seq"); + try { + while (cursor.moveToNext()) { + loadMessagePart(message, partById, cursor); + } + } finally { + cursor.close(); + } + } + + private void loadMessagePart(LocalMessage message, Map partById, Cursor cursor) + throws MessagingException { + + long id = cursor.getLong(0); + long parentId = cursor.getLong(2); + String mimeType = cursor.getString(3); + byte[] header = cursor.getBlob(6); + + final Part part; + if (id == message.getMessagePartId()) { + part = message; + } else { + Part parentPart = partById.get(parentId); + if (parentPart == null) { + throw new IllegalStateException("Parent part not found"); + } + + String parentMimeType = parentPart.getMimeType(); + if (parentMimeType.startsWith("multipart/")) { + BodyPart bodyPart = new MimeBodyPart(); + ((Multipart) parentPart.getBody()).addBodyPart(bodyPart); + part = bodyPart; + } else if (parentMimeType.startsWith("message/")) { + Message innerMessage = new MimeMessage(); + parentPart.setBody(innerMessage); + part = innerMessage; + } else { + throw new IllegalStateException("Parent is neither a multipart nor a message"); + } + + parseHeaderBytes(part, header); + } + partById.put(id, part); + + boolean isMultipart = mimeType.startsWith("multipart/"); + if (isMultipart) { + byte[] preamble = cursor.getBlob(11); + byte[] epilogue = cursor.getBlob(12); + String boundary = cursor.getString(13); + + MimeMultipart multipart = new MimeMultipart(mimeType, boundary); + part.setBody(multipart); + multipart.setPreamble(preamble); + multipart.setEpilogue(epilogue); + } else { + String encoding = cursor.getString(7); + byte[] data = cursor.getBlob(10); + + Body body = new BinaryMemoryBody(data, encoding); + part.setBody(body); + } + } + + private void parseHeaderBytes(final Part part, byte[] header) throws MessagingException { + MimeConfig parserConfig = new MimeConfig(); + parserConfig.setMaxHeaderLen(-1); + parserConfig.setMaxLineLen(-1); + parserConfig.setMaxHeaderCount(-1); + MimeStreamParser parser = new MimeStreamParser(parserConfig); + parser.setContentHandler(new ContentHandler() { + @Override + public void field(Field rawField) throws MimeException { + String name = rawField.getName(); + String raw = rawField.getRaw().toString(); + try { + part.addRawHeader(name, raw); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + } + + @Override + public void startMessage() throws MimeException { /* do nothing */ } + + @Override + public void endMessage() throws MimeException { /* do nothing */ } + + @Override + public void startBodyPart() throws MimeException { /* do nothing */ } + + @Override + public void endBodyPart() throws MimeException { /* do nothing */ } + + @Override + public void startHeader() throws MimeException { /* do nothing */ } + + @Override + public void endHeader() throws MimeException { /* do nothing */ } + + @Override + public void preamble(InputStream is) throws MimeException, IOException { /* do nothing */ } + + @Override + public void epilogue(InputStream is) throws MimeException, IOException { /* do nothing */ } + + @Override + public void startMultipart(BodyDescriptor bd) throws MimeException { /* do nothing */ } + + @Override + public void endMultipart() throws MimeException { /* do nothing */ } + + @Override + public void body(BodyDescriptor bd, InputStream is) throws MimeException, IOException { /* do nothing */ } + + @Override + public void raw(InputStream is) throws MimeException, IOException { /* do nothing */ } + }); + + try { + parser.parse(new ByteArrayInputStream(header)); + } catch (MimeException me) { + throw new MessagingException("Error parsing headers", me); + } catch (IOException e) { + throw new MessagingException("I/O error parsing headers", e); } } @@ -813,49 +797,16 @@ public class LocalFolder extends Folder implements Serializable { "LocalStore.getMessages(int, int, MessageRetrievalListener) not yet implemented"); } - /** - * Populate the header fields of the given list of messages by reading - * the saved header data from the database. - * - * @param messages - * The messages whose headers should be loaded. - * @throws UnavailableStorageException - */ - void populateHeaders(final List messages) throws MessagingException { + void populateHeaders(final LocalMessage message) throws MessagingException { this.localStore.database.execute(false, new DbCallback() { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException, MessagingException { - Cursor cursor = null; - if (messages.isEmpty()) { - return null; - } + Cursor cursor = db.query("message_parts", new String[] { "header" }, "id = ?", + new String[] { Long.toString(message.getMessagePartId()) }, null, null, null); try { - Map popMessages = new HashMap(); - List ids = new ArrayList(); - StringBuilder questions = new StringBuilder(); - - for (int i = 0; i < messages.size(); i++) { - if (i != 0) { - questions.append(", "); - } - questions.append("?"); - LocalMessage message = messages.get(i); - Long id = message.getId(); - ids.add(Long.toString(id)); - popMessages.put(id, message); - } - - cursor = db.rawQuery( - "SELECT message_id, name, value FROM headers " + - "WHERE message_id in ( " + questions + ") ORDER BY id ASC", - ids.toArray(LocalStore.EMPTY_STRING_ARRAY)); - - while (cursor.moveToNext()) { - Long id = cursor.getLong(0); - String name = cursor.getString(1); - String value = cursor.getString(2); - //Log.i(K9.LOG_TAG, "Retrieved header name= " + name + ", value = " + value + " for message " + id); - popMessages.get(id).addHeader(name, value); + if (cursor.moveToFirst()) { + byte[] header = cursor.getBlob(0); + parseHeaderBytes(message, header); } } finally { Utility.closeQuietly(cursor); @@ -1225,7 +1176,8 @@ public class LocalFolder extends Folder implements Serializable { * @param copy * @return uidMap of srcUids -> destUids */ - private Map appendMessages(final List messages, final boolean copy) throws MessagingException { + private Map appendMessages(final List messages, final boolean copy) + throws MessagingException { open(OPEN_MODE_RW); try { final Map uidMap = new HashMap(); @@ -1234,136 +1186,7 @@ public class LocalFolder extends Folder implements Serializable { public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { try { for (Message message : messages) { - long oldMessageId = -1; - String uid = message.getUid(); - if (uid == null || copy) { - /* - * Create a new message in the database - */ - String randomLocalUid = K9.LOCAL_UID_PREFIX + - UUID.randomUUID().toString(); - - if (copy) { - // Save mapping: source UID -> target UID - uidMap.put(uid, randomLocalUid); - } else { - // Modify the Message instance to reference the new UID - message.setUid(randomLocalUid); - } - - // The message will be saved with the newly generated UID - uid = randomLocalUid; - } else { - /* - * Replace an existing message in the database - */ - LocalMessage oldMessage = getMessage(uid); - - if (oldMessage != null) { - oldMessageId = oldMessage.getId(); - } - - deleteAttachments(message.getUid()); - } - - long rootId = -1; - long parentId = -1; - - if (oldMessageId == -1) { - // This is a new message. Do the message threading. - ThreadInfo threadInfo = doMessageThreading(db, message); - oldMessageId = threadInfo.msgId; - rootId = threadInfo.rootId; - parentId = threadInfo.parentId; - } - - boolean isDraft = (message.getHeader(K9.IDENTITY_HEADER) != null); - - List attachments; - String text; - String html; - if (isDraft) { - // Don't modify the text/plain or text/html part of our own - // draft messages because this will cause the values stored in - // the identity header to be wrong. - ViewableContainer container = - LocalMessageExtractor.extractPartsFromDraft(message); - - text = container.text; - html = container.html; - attachments = container.attachments; - } else { - ViewableContainer container = - LocalMessageExtractor.extractTextAndAttachments(LocalFolder.this.localStore.context, message); - - attachments = container.attachments; - text = container.text; - html = HtmlConverter.convertEmoji2Img(container.html); - } - - String preview = Message.calculateContentPreview(text); - - try { - ContentValues cv = new ContentValues(); - cv.put("uid", uid); - cv.put("subject", message.getSubject()); - cv.put("sender_list", Address.pack(message.getFrom())); - cv.put("date", message.getSentDate() == null - ? System.currentTimeMillis() : message.getSentDate().getTime()); - cv.put("flags", LocalFolder.this.localStore.serializeFlags(message.getFlags())); - cv.put("deleted", message.isSet(Flag.DELETED) ? 1 : 0); - cv.put("read", message.isSet(Flag.SEEN) ? 1 : 0); - cv.put("flagged", message.isSet(Flag.FLAGGED) ? 1 : 0); - cv.put("answered", message.isSet(Flag.ANSWERED) ? 1 : 0); - cv.put("forwarded", message.isSet(Flag.FORWARDED) ? 1 : 0); - cv.put("folder_id", mFolderId); - cv.put("to_list", Address.pack(message.getRecipients(RecipientType.TO))); - cv.put("cc_list", Address.pack(message.getRecipients(RecipientType.CC))); - cv.put("bcc_list", Address.pack(message.getRecipients(RecipientType.BCC))); - cv.put("html_content", html.length() > 0 ? html : null); - cv.put("text_content", text.length() > 0 ? text : null); - cv.put("preview", preview.length() > 0 ? preview : null); - cv.put("reply_to_list", Address.pack(message.getReplyTo())); - cv.put("attachment_count", attachments.size()); - cv.put("internal_date", message.getInternalDate() == null - ? System.currentTimeMillis() : message.getInternalDate().getTime()); - cv.put("mime_type", message.getMimeType()); - cv.put("empty", 0); - - String messageId = message.getMessageId(); - if (messageId != null) { - cv.put("message_id", messageId); - } - - long msgId; - - if (oldMessageId == -1) { - msgId = db.insert("messages", "uid", cv); - - // Create entry in 'threads' table - cv.clear(); - cv.put("message_id", msgId); - - if (rootId != -1) { - cv.put("root", rootId); - } - if (parentId != -1) { - cv.put("parent", parentId); - } - - db.insert("threads", null, cv); - } else { - db.update("messages", cv, "id = ?", new String[] { Long.toString(oldMessageId) }); - msgId = oldMessageId; - } - - for (Part attachment : attachments) { - saveAttachment(msgId, attachment, copy); - } - saveHeaders(msgId, (MimeMessage)message); - } catch (Exception e) { - throw new MessagingException("Error appending message", e); - } + saveMessage(db, message, copy, uidMap); } } catch (MessagingException e) { throw new WrappedException(e); @@ -1376,7 +1199,212 @@ public class LocalFolder extends Folder implements Serializable { return uidMap; } catch (WrappedException e) { - throw(MessagingException) e.getCause(); + throw (MessagingException) e.getCause(); + } + } + + protected void saveMessage(SQLiteDatabase db, Message message, boolean copy, Map uidMap) + throws MessagingException { + if (!(message instanceof MimeMessage)) { + throw new Error("LocalStore can only store Messages that extend MimeMessage"); + } + + long oldMessageId = -1; + String uid = message.getUid(); + boolean shouldCreateNewMessage = uid == null || copy; + if (shouldCreateNewMessage) { + String randomLocalUid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString(); + + if (copy) { + // Save mapping: source UID -> target UID + uidMap.put(uid, randomLocalUid); + } else { + // Modify the Message instance to reference the new UID + message.setUid(randomLocalUid); + } + + // The message will be saved with the newly generated UID + uid = randomLocalUid; + } else { + LocalMessage oldMessage = getMessage(uid); + + if (oldMessage != null) { + oldMessageId = oldMessage.getId(); + } + + //FIXME + deleteAttachments(message.getUid()); + } + + long rootId = -1; + long parentId = -1; + + if (oldMessageId == -1) { + // This is a new message. Do the message threading. + ThreadInfo threadInfo = doMessageThreading(db, message); + oldMessageId = threadInfo.msgId; + rootId = threadInfo.rootId; + parentId = threadInfo.parentId; + } + + //TODO: construct message preview + //TODO: get attachment count + + try { + long rootMessagePartId = saveMessageParts(db, message); + + ContentValues cv = new ContentValues(); + cv.put("message_part_id", rootMessagePartId); + cv.put("uid", uid); + cv.put("subject", message.getSubject()); + cv.put("sender_list", Address.pack(message.getFrom())); + cv.put("date", message.getSentDate() == null + ? System.currentTimeMillis() : message.getSentDate().getTime()); + cv.put("flags", this.localStore.serializeFlags(message.getFlags())); + cv.put("deleted", message.isSet(Flag.DELETED) ? 1 : 0); + cv.put("read", message.isSet(Flag.SEEN) ? 1 : 0); + cv.put("flagged", message.isSet(Flag.FLAGGED) ? 1 : 0); + cv.put("answered", message.isSet(Flag.ANSWERED) ? 1 : 0); + cv.put("forwarded", message.isSet(Flag.FORWARDED) ? 1 : 0); + cv.put("folder_id", mFolderId); + cv.put("to_list", Address.pack(message.getRecipients(RecipientType.TO))); + cv.put("cc_list", Address.pack(message.getRecipients(RecipientType.CC))); + cv.put("bcc_list", Address.pack(message.getRecipients(RecipientType.BCC))); + cv.put("preview", ""); //FIXME + cv.put("reply_to_list", Address.pack(message.getReplyTo())); + cv.put("attachment_count", 0); //FIXME + cv.put("internal_date", message.getInternalDate() == null + ? System.currentTimeMillis() : message.getInternalDate().getTime()); + cv.put("mime_type", message.getMimeType()); + cv.put("empty", 0); + + String messageId = message.getMessageId(); + if (messageId != null) { + cv.put("message_id", messageId); + } + + if (oldMessageId == -1) { + long msgId = db.insert("messages", "uid", cv); + + // Create entry in 'threads' table + cv.clear(); + cv.put("message_id", msgId); + + if (rootId != -1) { + cv.put("root", rootId); + } + if (parentId != -1) { + cv.put("parent", parentId); + } + + db.insert("threads", null, cv); + } else { + db.update("messages", cv, "id = ?", new String[] { Long.toString(oldMessageId) }); + } + } catch (Exception e) { + throw new MessagingException("Error appending message", e); + } + } + + private long saveMessageParts(SQLiteDatabase db, Message message) throws IOException, MessagingException { + long rootMessagePartId = saveMessagePart(db, new PartContainer(-1, message), -1, 0); + + Stack partsToSave = new Stack(); + addChildrenToStack(partsToSave, message, rootMessagePartId); + + int order = 1; + while (!partsToSave.isEmpty()) { + PartContainer partContainer = partsToSave.pop(); + long messagePartId = saveMessagePart(db, partContainer, rootMessagePartId, order); + order++; + + addChildrenToStack(partsToSave, partContainer.part, messagePartId); + } + + return rootMessagePartId; + } + + private long saveMessagePart(SQLiteDatabase db, PartContainer partContainer, long rootMessagePartId, int order) + throws IOException, MessagingException { + + Part part = partContainer.part; + + byte[] headerBytes = getHeaderBytes(part); + + ContentValues cv = new ContentValues(); + if (rootMessagePartId != -1) { + cv.put("root", rootMessagePartId); + } + cv.put("parent", partContainer.parent); + cv.put("seq", order); + cv.put("mime_type", part.getMimeType()); + cv.put("header", headerBytes); + cv.put("type", MessagePartType.UNKNOWN); + + Body body = part.getBody(); + if (body instanceof Multipart) { + cv.put("data_location", DataLocation.IN_DATABASE); + + Multipart multipart = (Multipart) body; + cv.put("preamble", multipart.getPreamble()); + cv.put("epilogue", multipart.getEpilogue()); + cv.put("boundary", multipart.getBoundary()); + } else if (body == null) { + //TODO: deal with missing parts + cv.put("data_location", DataLocation.MISSING); + } else { + cv.put("data_location", DataLocation.IN_DATABASE); + + byte[] bodyData = getBodyBytes(body); + String encoding = getTransferEncoding(part); + + cv.put("encoding", encoding); + cv.put("data", bodyData); + cv.put("content_id", part.getContentId()); + } + + return db.insertOrThrow("message_parts", null, cv); + } + + private byte[] getHeaderBytes(Part part) throws IOException, MessagingException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + part.writeHeaderTo(output); + return output.toByteArray(); + } + + private byte[] getBodyBytes(Body body) throws IOException, MessagingException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + body.writeTo(output); + return output.toByteArray(); + } + + private String getTransferEncoding(Part part) throws MessagingException { + String[] contentTransferEncoding = part.getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING); + if (contentTransferEncoding != null && contentTransferEncoding.length > 0) { + return contentTransferEncoding[0].toLowerCase(Locale.US); + } + + return MimeUtil.ENC_7BIT; + } + + private void addChildrenToStack(Stack stack, Part part, long parentMessageId) { + Body body = part.getBody(); + if (body instanceof Multipart) { + Multipart multipart = (Multipart) body; + for (int i = multipart.getCount() - 1; i >= 0; i--) { + BodyPart childPart = multipart.getBodyPart(i); + stack.push(new PartContainer(parentMessageId, childPart)); + } + } + } + + private static class PartContainer { + public final long parent; + public final Part part; + + PartContainer(long parent, Part part) { + this.parent = parent; + this.part = part; } } @@ -2191,4 +2219,21 @@ public class LocalFolder extends Folder implements Serializable { private Account getAccount() { return localStore.getAccount(); } + + // Note: The contents of the 'message_parts' table depend on these values. + private static class MessagePartType { + static final int UNKNOWN = 0; + static final int ALTERNATIVE_PLAIN = 1; + static final int ALTERNATIVE_HTML = 2; + static final int TEXT = 3; + static final int RELATED = 4; + static final int ATTACHMENT = 5; + } + + // Note: The contents of the 'message_parts' table depend on these values. + private static class DataLocation { + static final int MISSING = 0; + static final int IN_DATABASE = 1; + static final int ON_DISK = 2; + } } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java index a69fa751f..0623288c6 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java @@ -2,9 +2,7 @@ package com.fsck.k9.mailstore; import java.io.IOException; import java.io.OutputStream; -import java.util.ArrayList; import java.util.Date; -import java.util.List; import java.util.Set; import android.content.ContentValues; @@ -40,6 +38,8 @@ public class LocalMessage extends MimeMessage { private long mThreadId; private long mRootId; + private long messagePartId; + private String mimeType; private LocalMessage(LocalStore localStore) { this.localStore = localStore; @@ -110,6 +110,18 @@ public class LocalMessage extends MimeMessage { setFlagInternal(Flag.FLAGGED, flagged); setFlagInternal(Flag.ANSWERED, answered); setFlagInternal(Flag.FORWARDED, forwarded); + + messagePartId = cursor.getLong(22); + mimeType = cursor.getString(23); + } + + long getMessagePartId() { + return messagePartId; + } + + @Override + public String getMimeType() { + return mimeType; } /** @@ -477,23 +489,8 @@ public class LocalMessage extends MimeMessage { } private void loadHeaders() throws MessagingException { - List messages = new ArrayList(); - messages.add(this); - mHeadersLoaded = true; // set true before calling populate headers to stop recursion - getFolder().populateHeaders(messages); - - } - - @Override - public void addHeader(String name, String value) throws MessagingException { - if (!mHeadersLoaded) - loadHeaders(); - super.addHeader(name, value); - } - - @Override - public void addRawHeader(String name, String raw) { - throw new RuntimeException("Not supported"); + mHeadersLoaded = true; + getFolder().populateHeaders(this); } @Override diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java index 41b97e742..369b69703 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -76,7 +76,7 @@ public class LocalStore extends Store implements Serializable { "subject, sender_list, date, uid, flags, messages.id, to_list, cc_list, " + "bcc_list, reply_to_list, attachment_count, internal_date, messages.message_id, " + "folder_id, preview, threads.id, threads.root, deleted, read, flagged, answered, " + - "forwarded "; + "forwarded, message_part_id, mime_type "; static final String GET_FOLDER_COLS = "folders.id, name, visible_limit, last_updated, status, push_state, last_pushed, " + @@ -119,7 +119,7 @@ public class LocalStore extends Store implements Serializable { */ private static final int THREAD_FLAG_UPDATE_BATCH_SIZE = 500; - public static final int DB_VERSION = 50; + public static final int DB_VERSION = 51; public static String getColumnNameForFlag(Flag flag) { diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java b/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java index 39bed6b3e..37478cf6b 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java @@ -83,8 +83,6 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition { "cc_list TEXT, " + "bcc_list TEXT, " + "reply_to_list TEXT, " + - "html_content TEXT, " + - "text_content TEXT, " + "attachment_count INTEGER, " + "internal_date INTEGER, " + "message_id TEXT, " + @@ -95,12 +93,36 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition { "read INTEGER default 0, " + "flagged INTEGER default 0, " + "answered INTEGER default 0, " + - "forwarded INTEGER default 0" + + "forwarded INTEGER default 0, " + + "message_part_id INTEGER" + ")"); - db.execSQL("DROP TABLE IF EXISTS headers"); - db.execSQL("CREATE TABLE headers (id INTEGER PRIMARY KEY, message_id INTEGER, name TEXT, value TEXT)"); - db.execSQL("CREATE INDEX IF NOT EXISTS header_folder ON headers (message_id)"); + db.execSQL("CREATE TABLE message_parts (" + + "id INTEGER PRIMARY KEY, " + + "type INTEGER NOT NULL, " + + "root INTEGER, " + + "parent INTEGER NOT NULL, " + + "seq INTEGER NOT NULL, " + + "mime_type TEXT, " + + "decoded_body_size INTEGER, " + + "display_name TEXT, " + + "header TEXT, " + + "encoding TEXT, " + + "charset TEXT, " + + "data_location INTEGER NOT NULL, " + + "data TEXT, " + + "preamble TEXT, " + + "epilogue TEXT, " + + "boundary TEXT, " + + "content_id TEXT, " + + "server_extra TEXT" + + ")"); + + db.execSQL("CREATE TRIGGER set_message_part_root " + + "AFTER INSERT ON message_parts " + + "BEGIN " + + "UPDATE message_parts SET root=id WHERE root IS NULL AND ROWID = NEW.ROWID; " + + "END"); db.execSQL("CREATE INDEX IF NOT EXISTS msg_uid ON messages (uid, folder_id)"); db.execSQL("DROP INDEX IF EXISTS msg_folder_id"); @@ -541,6 +563,9 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition { db.update("folders", cv, "name = ?", new String[] { this.localStore.getAccount().getInboxFolderName() }); } + if (db.getVersion() < 51) { + throw new IllegalStateException("Database upgrade not supported yet!"); + } } db.setVersion(LocalStore.DB_VERSION); From 30e37000f93e06c6b1d699b3e80c19f49e9436ad Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 6 Jan 2015 21:36:31 +0100 Subject: [PATCH 007/152] Remove remnants of the "headers" table --- .../com/fsck/k9/mailstore/LocalFolder.java | 52 ++----------------- .../com/fsck/k9/mailstore/LocalMessage.java | 1 - .../k9/mailstore/StoreSchemaDefinition.java | 3 +- 3 files changed, 4 insertions(+), 52 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 38688de64..567de72d4 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -1472,7 +1472,9 @@ public class LocalFolder extends Folder implements Serializable { Part attachment = attachments.get(i); saveAttachment(message.getId(), attachment, false); } - saveHeaders(message.getId(), message); + + //FIXME + //saveHeaders(message.getId(), message); } catch (Exception e) { throw new MessagingException("Error appending message", e); } @@ -1489,54 +1491,6 @@ public class LocalFolder extends Folder implements Serializable { this.localStore.notifyChange(); } - /** - * Save the headers of the given message. Note that the message is not - * necessarily a {@link LocalMessage} instance. - * @param id - * @param message - * @throws com.fsck.k9.mail.MessagingException - */ - private void saveHeaders(final long id, final MimeMessage message) throws MessagingException { - this.localStore.database.execute(true, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException, MessagingException { - - deleteHeaders(id); - for (String name : message.getHeaderNames()) { - String[] values = message.getHeader(name); - for (String value : values) { - ContentValues cv = new ContentValues(); - cv.put("message_id", id); - cv.put("name", name); - cv.put("value", value); - db.insert("headers", "name", cv); - } - } - - // Remember that all headers for this message have been saved, so it is - // not necessary to download them again in case the user wants to see all headers. - List appendedFlags = new ArrayList(); - appendedFlags.addAll(message.getFlags()); - appendedFlags.add(Flag.X_GOT_ALL_HEADERS); - - db.execSQL("UPDATE messages " + "SET flags = ? " + " WHERE id = ?", - new Object[] { LocalFolder.this.localStore.serializeFlags(appendedFlags), id }); - - return null; - } - }); - } - - void deleteHeaders(final long id) throws MessagingException { - this.localStore.database.execute(false, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - db.execSQL("DELETE FROM headers WHERE message_id = ?", new Object[] { id }); - return null; - } - }); - } - /** * @param messageId * @param attachment diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java index 0623288c6..9c3be5752 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java @@ -341,7 +341,6 @@ public class LocalMessage extends MimeMessage { } catch (WrappedException e) { throw(MessagingException) e.getCause(); } - ((LocalFolder)mFolder).deleteHeaders(mId); this.localStore.notifyChange(); } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java b/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java index 37478cf6b..2b727dc82 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java @@ -180,8 +180,7 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition { db.execSQL("CREATE TRIGGER delete_folder BEFORE DELETE ON folders BEGIN DELETE FROM messages WHERE old.id = folder_id; END;"); db.execSQL("DROP TRIGGER IF EXISTS delete_message"); - db.execSQL("CREATE TRIGGER delete_message BEFORE DELETE ON messages BEGIN DELETE FROM attachments WHERE old.id = message_id; " - + "DELETE FROM headers where old.id = message_id; END;"); + db.execSQL("CREATE TRIGGER delete_message BEFORE DELETE ON messages BEGIN DELETE FROM attachments WHERE old.id = message_id; END;"); } else { // in the case that we're starting out at 29 or newer, run all the needed updates From 1a5ecfea1dc6a7a7237a719089fd62980d1106aa Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 6 Jan 2015 23:48:22 +0100 Subject: [PATCH 008/152] Also delete local messages when using "clear messages" on an account We have been throwing away all attachments already, so it doesn't make too much sense to keep local messages. And when we're not keeping local messages we can remove all entries from the 'threads' table. --- .../com/fsck/k9/mailstore/LocalStore.java | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java index 369b69703..2be6d0bac 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -285,24 +285,16 @@ public class LocalStore extends Store implements Serializable { Log.i(K9.LOG_TAG, "After prune / before clear size = " + getSize()); } - // don't delete messages that are Local, since there is no copy on the server. - // Don't delete deleted messages. They are essentially placeholders for UIDs of messages that have - // been deleted locally. They take up insignificant space + database.execute(false, new DbCallback() { @Override public Void doDbWork(final SQLiteDatabase db) { - // Delete entries from 'threads' table - db.execSQL("DELETE FROM threads WHERE message_id IN " + - "(SELECT id FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%')"); + // We don't care about threads of deleted messages, so delete the whole table. + db.delete("threads", null, null); - // Set 'root' and 'parent' of remaining entries in 'thread' table to 'NULL' to make - // sure the thread structure is in a valid state (this may destroy existing valid - // thread trees, but is much faster than adjusting the tree by removing messages - // one by one). - db.execSQL("UPDATE threads SET root=id, parent=NULL"); - - // Delete entries from 'messages' table - db.execSQL("DELETE FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%'"); + // Don't delete deleted messages. They are essentially placeholders for UIDs of messages that have + // been deleted locally. + db.delete("messages", "deleted = 0", null); return null; } }); From 34b5d56ab188c34fae42c92216fbeddc07b8b6da Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 6 Jan 2015 23:59:58 +0100 Subject: [PATCH 009/152] Get rid of 'attachments' table --- .../k9/controller/MessagingController.java | 2 +- .../com/fsck/k9/mailstore/LocalFolder.java | 410 +++--------------- .../com/fsck/k9/mailstore/LocalMessage.java | 36 +- .../com/fsck/k9/mailstore/LocalStore.java | 119 ++--- .../k9/mailstore/StoreSchemaDefinition.java | 11 +- 5 files changed, 122 insertions(+), 456 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java b/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java index 8d0dd8f20..95c38f10e 100644 --- a/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -3194,7 +3194,7 @@ public class MessagingController implements Runnable { MimeMessageHelper.setBody(remoteMessage, message.getBody()); remoteFolder.fetchPart(remoteMessage, part, null); - localFolder.updateMessage((LocalMessage)message); + localFolder.addPartToMessage((LocalMessage) message, part); for (MessagingListener l : getListeners(listener)) { l.loadAttachmentFinished(account, message, part, tag); } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 567de72d4..a4eb27112 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -4,7 +4,6 @@ package com.fsck.k9.mailstore; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; @@ -19,20 +18,17 @@ import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.UUID; -import java.util.regex.Pattern; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; import android.util.Log; import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.activity.Search; -import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Body; @@ -49,13 +45,10 @@ import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMessage; -import com.fsck.k9.mail.internet.MimeMessageHelper; import com.fsck.k9.mail.internet.MimeMultipart; -import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.WrappedException; import com.fsck.k9.provider.AttachmentProvider; -import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.parser.ContentHandler; import org.apache.james.mime4j.parser.MimeStreamParser; @@ -1230,10 +1223,10 @@ public class LocalFolder extends Folder implements Serializable { if (oldMessage != null) { oldMessageId = oldMessage.getId(); - } - //FIXME - deleteAttachments(message.getUid()); + long oldRootMessagePartId = oldMessage.getMessagePartId(); + deleteMessagePartsAndDataFromDisk(oldRootMessagePartId); + } } long rootId = -1; @@ -1408,274 +1401,11 @@ public class LocalFolder extends Folder implements Serializable { } } - /** - * Update the given message in the LocalStore without first deleting the existing - * message (contrast with appendMessages). This method is used to store changes - * to the given message while updating attachments and not removing existing - * attachment data. - * TODO In the future this method should be combined with appendMessages since the Message - * contains enough data to decide what to do. - * @param message - * @throws MessagingException - */ - public void updateMessage(final LocalMessage message) throws MessagingException { + public void addPartToMessage(final LocalMessage message, final Part part) throws MessagingException { open(OPEN_MODE_RW); - try { - this.localStore.database.execute(false, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - try { - ViewableContainer container = - LocalMessageExtractor.extractTextAndAttachments(LocalFolder.this.localStore.context, message); + throw new RuntimeException("Not implemented yet"); - List attachments = container.attachments; - String text = container.text; - String html = HtmlConverter.convertEmoji2Img(container.html); - - String preview = Message.calculateContentPreview(text); - - try { - db.execSQL("UPDATE messages SET " - + "uid = ?, subject = ?, sender_list = ?, date = ?, flags = ?, " - + "folder_id = ?, to_list = ?, cc_list = ?, bcc_list = ?, " - + "html_content = ?, text_content = ?, preview = ?, reply_to_list = ?, " - + "attachment_count = ?, read = ?, flagged = ?, answered = ?, forwarded = ? " - + "WHERE id = ?", - new Object[] { - message.getUid(), - message.getSubject(), - Address.pack(message.getFrom()), - message.getSentDate() == null ? System - .currentTimeMillis() : message.getSentDate() - .getTime(), - LocalFolder.this.localStore.serializeFlags(message.getFlags()), - mFolderId, - Address.pack(message - .getRecipients(RecipientType.TO)), - Address.pack(message - .getRecipients(RecipientType.CC)), - Address.pack(message - .getRecipients(RecipientType.BCC)), - html.length() > 0 ? html : null, - text.length() > 0 ? text : null, - preview.length() > 0 ? preview : null, - Address.pack(message.getReplyTo()), - attachments.size(), - message.isSet(Flag.SEEN) ? 1 : 0, - message.isSet(Flag.FLAGGED) ? 1 : 0, - message.isSet(Flag.ANSWERED) ? 1 : 0, - message.isSet(Flag.FORWARDED) ? 1 : 0, - message.getId() - }); - - for (int i = 0, count = attachments.size(); i < count; i++) { - Part attachment = attachments.get(i); - saveAttachment(message.getId(), attachment, false); - } - - //FIXME - //saveHeaders(message.getId(), message); - } catch (Exception e) { - throw new MessagingException("Error appending message", e); - } - } catch (MessagingException e) { - throw new WrappedException(e); - } - return null; - } - }); - } catch (WrappedException e) { - throw(MessagingException) e.getCause(); - } - - this.localStore.notifyChange(); - } - - /** - * @param messageId - * @param attachment - * @param saveAsNew - * @throws IOException - * @throws MessagingException - */ - private void saveAttachment(final long messageId, final Part attachment, final boolean saveAsNew) - throws IOException, MessagingException { - try { - this.localStore.database.execute(true, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - try { - long attachmentId = -1; - Uri contentUri = null; - int size = -1; - File tempAttachmentFile = null; - - if ((!saveAsNew) && (attachment instanceof LocalAttachmentBodyPart)) { - attachmentId = ((LocalAttachmentBodyPart) attachment).getAttachmentId(); - } - - final File attachmentDirectory = StorageManager.getInstance(LocalFolder.this.localStore.context).getAttachmentDirectory(LocalFolder.this.localStore.uUid, LocalFolder.this.localStore.database.getStorageProviderId()); - if (attachment.getBody() != null) { - Body body = attachment.getBody(); - if (body instanceof LocalAttachmentBody) { - contentUri = ((LocalAttachmentBody) body).getContentUri(); - } else if (body instanceof Message) { - // It's a message, so use Message.writeTo() to output the - // message including all children. - Message message = (Message) body; - tempAttachmentFile = File.createTempFile("att", null, attachmentDirectory); - FileOutputStream out = new FileOutputStream(tempAttachmentFile); - try { - message.writeTo(out); - } finally { - out.close(); - } - size = (int) (tempAttachmentFile.length() & 0x7FFFFFFFL); - } else { - /* - * If the attachment has a body we're expected to save it into the local store - * so we copy the data into a cached attachment file. - */ - InputStream in = MimeUtility.decodeBody(attachment.getBody()); - try { - tempAttachmentFile = File.createTempFile("att", null, attachmentDirectory); - FileOutputStream out = new FileOutputStream(tempAttachmentFile); - try { - size = IOUtils.copy(in, out); - } finally { - out.close(); - } - } finally { - try { in.close(); } catch (Throwable ignore) {} - } - } - } - - if (size == -1) { - /* - * If the attachment is not yet downloaded see if we can pull a size - * off the Content-Disposition. - */ - String disposition = attachment.getDisposition(); - if (disposition != null) { - String sizeParam = MimeUtility.getHeaderParameter(disposition, "size"); - if (sizeParam != null) { - try { - size = Integer.parseInt(sizeParam); - } catch (NumberFormatException e) { /* Ignore */ } - } - } - } - if (size == -1) { - size = 0; - } - - String storeData = - Utility.combine(attachment.getHeader( - MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA), ','); - - String name = MimeUtility.getHeaderParameter(attachment.getContentType(), "name"); - String contentId = MimeUtility.getHeaderParameter(attachment.getContentId(), null); - - String contentDisposition = MimeUtility.unfoldAndDecode(attachment.getDisposition()); - String dispositionType = contentDisposition; - - if (dispositionType != null) { - int pos = dispositionType.indexOf(';'); - if (pos != -1) { - // extract the disposition-type, "attachment", "inline" or extension-token (see the RFC 2183) - dispositionType = dispositionType.substring(0, pos); - } - } - - if (name == null && contentDisposition != null) { - name = MimeUtility.getHeaderParameter(contentDisposition, "filename"); - } - if (attachmentId == -1) { - ContentValues cv = new ContentValues(); - cv.put("message_id", messageId); - cv.put("content_uri", contentUri != null ? contentUri.toString() : null); - cv.put("store_data", storeData); - cv.put("size", size); - cv.put("name", name); - cv.put("mime_type", attachment.getMimeType()); - cv.put("content_id", contentId); - cv.put("content_disposition", dispositionType); - - attachmentId = db.insert("attachments", "message_id", cv); - } else { - ContentValues cv = new ContentValues(); - cv.put("content_uri", contentUri != null ? contentUri.toString() : null); - cv.put("size", size); - db.update("attachments", cv, "id = ?", new String[] - { Long.toString(attachmentId) }); - } - - if (attachmentId != -1 && tempAttachmentFile != null) { - File attachmentFile = new File(attachmentDirectory, Long.toString(attachmentId)); - tempAttachmentFile.renameTo(attachmentFile); - contentUri = AttachmentProvider.getAttachmentUri( - getAccount(), - attachmentId); - if (MimeUtil.isMessage(attachment.getMimeType())) { - LocalAttachmentMessageBody body = new LocalAttachmentMessageBody( - contentUri, LocalFolder.this.localStore.context); - MimeMessageHelper.setBody(attachment, body); - } else { - LocalAttachmentBody body = new LocalAttachmentBody( - contentUri, LocalFolder.this.localStore.context); - MimeMessageHelper.setBody(attachment, body); - } - ContentValues cv = new ContentValues(); - cv.put("content_uri", contentUri != null ? contentUri.toString() : null); - db.update("attachments", cv, "id = ?", new String[] - { Long.toString(attachmentId) }); - } - - /* The message has attachment with Content-ID */ - if (contentId != null && contentUri != null) { - Cursor cursor = db.query("messages", new String[] - { "html_content" }, "id = ?", new String[] - { Long.toString(messageId) }, null, null, null); - try { - if (cursor.moveToNext()) { - String htmlContent = cursor.getString(0); - - if (htmlContent != null) { - String newHtmlContent = htmlContent.replaceAll( - Pattern.quote("cid:" + contentId), - contentUri.toString()); - - ContentValues cv = new ContentValues(); - cv.put("html_content", newHtmlContent); - db.update("messages", cv, "id = ?", new String[] - { Long.toString(messageId) }); - } - } - } finally { - Utility.closeQuietly(cursor); - } - } - - if (attachmentId != -1 && attachment instanceof LocalAttachmentBodyPart) { - ((LocalAttachmentBodyPart) attachment).setAttachmentId(attachmentId); - } - return null; - } catch (MessagingException e) { - throw new WrappedException(e); - } catch (IOException e) { - throw new WrappedException(e); - } - } - }); - } catch (WrappedException e) { - final Throwable cause = e.getCause(); - if (cause instanceof IOException) { - throw (IOException) cause; - } - - throw (MessagingException) cause; - } +// localStore.notifyChange(); } /** @@ -1770,21 +1500,18 @@ public class LocalFolder extends Folder implements Serializable { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException { try { - // Get UIDs for all messages to delete - Cursor cursor = db.query("messages", new String[] { "uid" }, + Cursor cursor = db.query("messages", new String[] { "message_part_id" }, "folder_id = ? AND (empty IS NULL OR empty != 1)", folderIdArg, null, null, null); - try { - // Delete attachments of these messages while (cursor.moveToNext()) { - deleteAttachments(cursor.getString(0)); + long messagePartId = cursor.getLong(0); + deleteMessageDataFromDisk(messagePartId); } } finally { cursor.close(); } - // Delete entries in 'threads' and 'messages' db.execSQL("DELETE FROM threads WHERE message_id IN " + "(SELECT id FROM messages WHERE folder_id = ?)", folderIdArg); db.execSQL("DELETE FROM messages WHERE folder_id = ?", folderIdArg); @@ -1816,9 +1543,9 @@ public class LocalFolder extends Folder implements Serializable { try { // We need to open the folder first to make sure we've got it's id open(OPEN_MODE_RO); - List messages = getMessages(null); - for (Message message : messages) { - deleteAttachments(message.getUid()); + List messages = getMessages(null); + for (LocalMessage message : messages) { + deleteMessageDataFromDisk(message.getMessagePartId()); } } catch (MessagingException e) { throw new WrappedException(e); @@ -1846,75 +1573,68 @@ public class LocalFolder extends Folder implements Serializable { return mName.hashCode(); } - void deleteAttachments(final long messageId) throws MessagingException { - open(OPEN_MODE_RW); - this.localStore.database.execute(false, new DbCallback() { + void deleteMessagePartsAndDataFromDisk(final long rootMessagePartId) throws MessagingException { + deleteMessageDataFromDisk(rootMessagePartId); + deleteMessageParts(rootMessagePartId); + } + + private void deleteMessageParts(final long rootMessagePartId) throws MessagingException { + localStore.database.execute(false, new DbCallback() { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - Cursor attachmentsCursor = null; - try { - String accountUuid = getAccountUuid(); - Context context = LocalFolder.this.localStore.context; - - // Get attachment IDs - String[] whereArgs = new String[] { Long.toString(messageId) }; - attachmentsCursor = db.query("attachments", new String[] { "id" }, - "message_id = ?", whereArgs, null, null, null); - - final File attachmentDirectory = StorageManager.getInstance(LocalFolder.this.localStore.context) - .getAttachmentDirectory(LocalFolder.this.localStore.uUid, LocalFolder.this.localStore.database.getStorageProviderId()); - - while (attachmentsCursor.moveToNext()) { - String attachmentId = Long.toString(attachmentsCursor.getLong(0)); - try { - // Delete stored attachment - File file = new File(attachmentDirectory, attachmentId); - if (file.exists()) { - file.delete(); - } - - // Delete thumbnail file - AttachmentProvider.deleteThumbnail(context, accountUuid, - attachmentId); - } catch (Exception e) { /* ignore */ } - } - - // Delete attachment metadata from the database - db.delete("attachments", "message_id = ?", whereArgs); - } finally { - Utility.closeQuietly(attachmentsCursor); - } + db.delete("message_parts", "root = ?", new String[] { Long.toString(rootMessagePartId) }); return null; } }); } - private void deleteAttachments(final String uid) throws MessagingException { - open(OPEN_MODE_RW); - try { - this.localStore.database.execute(false, new DbCallback() { - @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - Cursor messagesCursor = null; - try { - messagesCursor = db.query("messages", new String[] - { "id" }, "folder_id = ? AND uid = ?", new String[] - { Long.toString(mFolderId), uid }, null, null, null); - while (messagesCursor.moveToNext()) { - long messageId = messagesCursor.getLong(0); - deleteAttachments(messageId); + private void deleteMessageDataFromDisk(final long rootMessagePartId) throws MessagingException { + localStore.database.execute(false, new DbCallback() { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { + deleteMessagePartsFromDisk(db, rootMessagePartId); + deleteAttachmentThumbnailsFromDisk(db, rootMessagePartId); + return null; + } + }); + } - } - } catch (MessagingException e) { - throw new WrappedException(e); - } finally { - Utility.closeQuietly(messagesCursor); + private void deleteMessagePartsFromDisk(SQLiteDatabase db, long rootMessagePartId) { + File attachmentDirectory = StorageManager.getInstance(localStore.context) + .getAttachmentDirectory(getAccountUuid(), localStore.database.getStorageProviderId()); + + Cursor cursor = db.query("message_parts", new String[] { "id" }, + "root = ? AND data_location = " + DataLocation.ON_DISK, + new String[] { Long.toString(rootMessagePartId) }, null, null, null); + try { + while (cursor.moveToNext()) { + String messagePartId = cursor.getString(0); + File file = new File(attachmentDirectory, messagePartId); + if (file.exists()) { + if (!file.delete() && K9.DEBUG) { + Log.d(K9.LOG_TAG, "Couldn't delete message part file: " + file.getAbsolutePath()); } - return null; } - }); - } catch (WrappedException e) { - throw(MessagingException) e.getCause(); + } + } finally { + cursor.close(); + } + } + + private void deleteAttachmentThumbnailsFromDisk(SQLiteDatabase db, long rootMessagePartId) { + Context context = localStore.context; + String accountUuid = getAccountUuid(); + + Cursor cursor = db.query("message_parts", new String[] { "id" }, + "root = ? AND type = " + MessagePartType.ATTACHMENT, + new String[] { Long.toString(rootMessagePartId) }, null, null, null); + try { + while (cursor.moveToNext()) { + String messagePartId = cursor.getString(0); + AttachmentProvider.deleteThumbnail(context, accountUuid, messagePartId); + } + } finally { + cursor.close(); } } @@ -2185,7 +1905,7 @@ public class LocalFolder extends Folder implements Serializable { } // Note: The contents of the 'message_parts' table depend on these values. - private static class DataLocation { + static class DataLocation { static final int MISSING = 0; static final int IN_DATABASE = 1; static final int ON_DISK = 2; diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java index 9c3be5752..7a0fa44cb 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java @@ -290,23 +290,14 @@ public class LocalMessage extends MimeMessage { } /* - * If a message is being marked as deleted we want to clear out it's content - * and attachments as well. Delete will not actually remove the row since we need - * to retain the uid for synchronization purposes. + * If a message is being marked as deleted we want to clear out its content. Delete will not actually remove the + * row since we need to retain the UID for synchronization purposes. */ - private void delete() throws MessagingException - - { - /* - * Delete all of the message's content to save space. - */ + private void delete() throws MessagingException { try { - this.localStore.database.execute(true, new DbCallback() { + localStore.database.execute(true, new DbCallback() { @Override - public Void doDbWork(final SQLiteDatabase db) throws WrappedException, - UnavailableStorageException { - String[] idArg = new String[] { Long.toString(mId) }; - + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { ContentValues cv = new ContentValues(); cv.put("deleted", 1); cv.put("empty", 1); @@ -320,29 +311,24 @@ public class LocalMessage extends MimeMessage { cv.putNull("html_content"); cv.putNull("text_content"); cv.putNull("reply_to_list"); + cv.putNull("message_part_id"); - db.update("messages", cv, "id = ?", idArg); + db.update("messages", cv, "id = ?", new String[] { Long.toString(mId) }); - /* - * Delete all of the message's attachments to save space. - * We do this explicit deletion here because we're not deleting the record - * in messages, which means our ON DELETE trigger for messages won't cascade - */ try { - ((LocalFolder) mFolder).deleteAttachments(mId); + ((LocalFolder) mFolder).deleteMessagePartsAndDataFromDisk(messagePartId); } catch (MessagingException e) { throw new WrappedException(e); } - db.delete("attachments", "message_id = ?", idArg); return null; } }); } catch (WrappedException e) { - throw(MessagingException) e.getCause(); + throw (MessagingException) e.getCause(); } - this.localStore.notifyChange(); + localStore.notifyChange(); } /* @@ -360,7 +346,7 @@ public class LocalMessage extends MimeMessage { try { LocalFolder localFolder = (LocalFolder) mFolder; - localFolder.deleteAttachments(mId); + localFolder.deleteMessagePartsAndDataFromDisk(messagePartId); if (hasThreadChildren(db, mId)) { // This message has children in the thread structure so we need to diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java index 2be6d0bac..b12e0a936 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -21,8 +21,8 @@ import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.MessageRetrievalListener; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Store; +import com.fsck.k9.mailstore.LocalFolder.DataLocation; import com.fsck.k9.mailstore.StorageManager.StorageProvider; -import com.fsck.k9.mail.store.StoreConfig; import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.WrappedException; import com.fsck.k9.provider.EmailProvider; @@ -276,7 +276,7 @@ public class LocalStore extends Store implements Serializable { if (K9.DEBUG) Log.i(K9.LOG_TAG, "Before prune size = " + getSize()); - pruneCachedAttachments(true); + deleteAllMessageDataFromDisk(); if (K9.DEBUG) { Log.i(K9.LOG_TAG, "After prune / before compaction size = " + getSize()); @@ -398,73 +398,39 @@ public class LocalStore extends Store implements Serializable { database.recreate(); } - /** - * Deletes all cached attachments for the entire store. - * @param force - * @throws com.fsck.k9.mail.MessagingException - */ - //TODO this method seems to be only called with force=true, simplify accordingly - private void pruneCachedAttachments(final boolean force) throws MessagingException { + private void deleteAllMessageDataFromDisk() throws MessagingException { + markAllMessagePartsDataAsMissing(); + deleteAllMessagePartsDataFromDisk(); + } + + private void markAllMessagePartsDataAsMissing() throws MessagingException { database.execute(false, new DbCallback() { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException { - if (force) { - ContentValues cv = new ContentValues(); - cv.putNull("content_uri"); - db.update("attachments", cv, null, null); - } - final StorageManager storageManager = StorageManager.getInstance(context); - File[] files = storageManager.getAttachmentDirectory(uUid, database.getStorageProviderId()).listFiles(); - for (File file : files) { - if (file.exists()) { - if (!force) { - Cursor cursor = null; - try { - cursor = db.query( - "attachments", - new String[] { "store_data" }, - "id = ?", - new String[] { file.getName() }, - null, - null, - null); - if (cursor.moveToNext()) { - if (cursor.getString(0) == null) { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Attachment " + file.getAbsolutePath() + " has no store data, not deleting"); - /* - * If the attachment has no store data it is not recoverable, so - * we won't delete it. - */ - continue; - } - } - } finally { - Utility.closeQuietly(cursor); - } - } - if (!force) { - try { - ContentValues cv = new ContentValues(); - cv.putNull("content_uri"); - db.update("attachments", cv, "id = ?", new String[] { file.getName() }); - } catch (Exception e) { - /* - * If the row has gone away before we got to mark it not-downloaded that's - * okay. - */ - } - } - if (!file.delete()) { - file.deleteOnExit(); - } - } - } + ContentValues cv = new ContentValues(); + cv.put("data_location", DataLocation.MISSING); + db.update("message_parts", cv, null, null); + return null; } }); } + private void deleteAllMessagePartsDataFromDisk() { + final StorageManager storageManager = StorageManager.getInstance(context); + File attachmentDirectory = storageManager.getAttachmentDirectory(uUid, database.getStorageProviderId()); + File[] files = attachmentDirectory.listFiles(); + if (files == null) { + return; + } + + for (File file : files) { + if (file.exists() && !file.delete()) { + file.deleteOnExit(); + } + } + } + public void resetVisibleLimits(int visibleLimit) throws MessagingException { final ContentValues cv = new ContentValues(); cv.put("visible_limit", Integer.toString(visibleLimit)); @@ -672,32 +638,27 @@ public class LocalStore extends Store implements Serializable { return database.execute(false, new DbCallback() { @Override public AttachmentInfo doDbWork(final SQLiteDatabase db) throws WrappedException { - String name; - String type; - int size; - Cursor cursor = null; + Cursor cursor = db.query("message_parts", + new String[] { "display_name", "decoded_body_size", "mime_type" }, + "id = ?", + new String[] { attachmentId }, + null, null, null); try { - cursor = db.query( - "attachments", - new String[] { "name", "size", "mime_type" }, - "id = ?", - new String[] { attachmentId }, - null, - null, - null); if (!cursor.moveToFirst()) { return null; } - name = cursor.getString(0); - size = cursor.getInt(1); - type = cursor.getString(2); + String name = cursor.getString(0); + int size = cursor.getInt(1); + String mimeType = cursor.getString(2); + final AttachmentInfo attachmentInfo = new AttachmentInfo(); attachmentInfo.name = name; attachmentInfo.size = size; - attachmentInfo.type = type; + attachmentInfo.type = mimeType; + return attachmentInfo; } finally { - Utility.closeQuietly(cursor); + cursor.close(); } } }); @@ -705,7 +666,7 @@ public class LocalStore extends Store implements Serializable { public static class AttachmentInfo { public String name; - public int size; + public int size; //FIXME: use long public String type; } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java b/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java index 2b727dc82..d8be088ee 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java @@ -167,11 +167,6 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition { "UPDATE threads SET root=id WHERE root IS NULL AND ROWID = NEW.ROWID; " + "END"); - db.execSQL("DROP TABLE IF EXISTS attachments"); - db.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER," - + "store_data TEXT, content_uri TEXT, size INTEGER, name TEXT," - + "mime_type TEXT, content_id TEXT, content_disposition TEXT)"); - db.execSQL("DROP TABLE IF EXISTS pending_commands"); db.execSQL("CREATE TABLE pending_commands " + "(id INTEGER PRIMARY KEY, command TEXT, arguments TEXT)"); @@ -180,7 +175,11 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition { db.execSQL("CREATE TRIGGER delete_folder BEFORE DELETE ON folders BEGIN DELETE FROM messages WHERE old.id = folder_id; END;"); db.execSQL("DROP TRIGGER IF EXISTS delete_message"); - db.execSQL("CREATE TRIGGER delete_message BEFORE DELETE ON messages BEGIN DELETE FROM attachments WHERE old.id = message_id; END;"); + db.execSQL("CREATE TRIGGER delete_message " + + "BEFORE DELETE ON messages " + + "BEGIN " + + "DELETE FROM message_parts WHERE root = OLD.message_part_id;" + + "END"); } else { // in the case that we're starting out at 29 or newer, run all the needed updates From c5ba202a56ff433ad968be98d1316f034c7e46cb Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 7 Jan 2015 00:13:28 +0100 Subject: [PATCH 010/152] Code style fixes --- .../fsck/k9/provider/AttachmentProvider.java | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java b/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java index d59de5592..971af261c 100644 --- a/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java +++ b/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java @@ -1,5 +1,6 @@ package com.fsck.k9.provider; + import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; @@ -10,6 +11,7 @@ import android.graphics.BitmapFactory; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.util.Log; + import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.Preferences; @@ -22,9 +24,9 @@ import com.fsck.k9.mailstore.StorageManager; import java.io.*; import java.util.List; + /** * A simple ContentProvider that allows file access to attachments. - * *

* Warning! We make heavy assumptions about the Uris used by the {@link LocalStore} for an * {@link Account} here. @@ -38,8 +40,8 @@ public class AttachmentProvider extends ContentProvider { private static final String FORMAT_THUMBNAIL = "THUMBNAIL"; private static final String[] DEFAULT_PROJECTION = new String[] { - AttachmentProviderColumns._ID, - AttachmentProviderColumns.DATA, + AttachmentProviderColumns._ID, + AttachmentProviderColumns.DATA, }; public static class AttachmentProviderColumns { @@ -70,12 +72,12 @@ public class AttachmentProvider extends ContentProvider { public static Uri getAttachmentThumbnailUri(Account account, long id, int width, int height) { return CONTENT_URI.buildUpon() - .appendPath(account.getUuid()) - .appendPath(Long.toString(id)) - .appendPath(FORMAT_THUMBNAIL) - .appendPath(Integer.toString(width)) - .appendPath(Integer.toString(height)) - .build(); + .appendPath(account.getUuid()) + .appendPath(Long.toString(id)) + .appendPath(FORMAT_THUMBNAIL) + .appendPath(Integer.toString(width)) + .appendPath(Integer.toString(height)) + .build(); } public static void clear(Context context) { @@ -111,8 +113,7 @@ public class AttachmentProvider extends ContentProvider { } } - private static File getThumbnailFile(Context context, String accountUuid, - String attachmentId) { + private static File getThumbnailFile(Context context, String accountUuid, String attachmentId) { String filename = "thmb_" + accountUuid + "_" + attachmentId + ".tmp"; File dir = context.getCacheDir(); return new File(dir, filename); @@ -183,7 +184,9 @@ public class AttachmentProvider extends ContentProvider { } } } finally { - try { in.close(); } catch (Throwable ignore) { /* ignore */ } + try { + in.close(); + } catch (Throwable ignore) { /* ignore */ } } } catch (IOException ioe) { return null; @@ -197,8 +200,7 @@ public class AttachmentProvider extends ContentProvider { } @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { String[] columnNames = (projection == null) ? DEFAULT_PROJECTION : projection; @@ -309,8 +311,7 @@ public class AttachmentProvider extends ContentProvider { private Bitmap createImageThumbnail(InputStream data) { try { - Bitmap bitmap = BitmapFactory.decodeStream(data); - return bitmap; + return BitmapFactory.decodeStream(data); } catch (OutOfMemoryError oome) { /* * Improperly downloaded images, corrupt bitmaps and the like can commonly From e5f0bec6bcc27eae3b33107abcb4ff752f957ecf Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 7 Jan 2015 00:16:37 +0100 Subject: [PATCH 011/152] Get rid of "backward compatibility" in AttachmentProvider --- .../fsck/k9/provider/AttachmentProvider.java | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java b/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java index 971af261c..75fdcef35 100644 --- a/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java +++ b/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java @@ -146,12 +146,12 @@ public class AttachmentProvider extends ContentProvider { @Override public String getType(Uri uri) { List segments = uri.getPathSegments(); - String dbName = segments.get(0); + String accountUuid = segments.get(0); String id = segments.get(1); String format = segments.get(2); String mimeType = (segments.size() < 4) ? null : segments.get(3); - return getType(dbName, id, format, mimeType); + return getType(accountUuid, id, format, mimeType); } @Override @@ -205,18 +205,12 @@ public class AttachmentProvider extends ContentProvider { String[] columnNames = (projection == null) ? DEFAULT_PROJECTION : projection; List segments = uri.getPathSegments(); - String dbName = segments.get(0); + String accountUuid = segments.get(0); String id = segments.get(1); - // Versions of K-9 before 3.400 had a database name here, not an - // account UID, so implement a bit of backcompat - if (dbName.endsWith(".db")) { - dbName = dbName.substring(0, dbName.length() - 3); - } - final AttachmentInfo attachmentInfo; try { - final Account account = Preferences.getPreferences(getContext()).getAccount(dbName); + final Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid); attachmentInfo = LocalStore.getInstance(account, getContext()).getAttachmentInfo(id); } catch (MessagingException e) { Log.e(K9.LOG_TAG, "Unable to retrieve attachment info from local store for ID: " + id, e); @@ -263,12 +257,12 @@ public class AttachmentProvider extends ContentProvider { return null; } - private String getType(String dbName, String id, String format, String mimeType) { + private String getType(String accountUuid, String id, String format, String mimeType) { String type; if (FORMAT_THUMBNAIL.equals(format)) { type = "image/png"; } else { - final Account account = Preferences.getPreferences(getContext()).getAccount(dbName); + final Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid); try { final LocalStore localStore = LocalStore.getInstance(account, getContext()); @@ -288,10 +282,10 @@ public class AttachmentProvider extends ContentProvider { return type; } - private File getFile(String dbName, String id) throws FileNotFoundException { - Account account = Preferences.getPreferences(getContext()).getAccount(dbName); + private File getFile(String accountUuid, String id) throws FileNotFoundException { + Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid); - File attachmentsDir = StorageManager.getInstance(getContext()).getAttachmentDirectory(dbName, + File attachmentsDir = StorageManager.getInstance(getContext()).getAttachmentDirectory(accountUuid, account.getLocalStorageProviderId()); File file = new File(attachmentsDir, id); From ce862c88f8023d62309d0553fa7bfd3d4e885305 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 7 Jan 2015 02:29:56 +0100 Subject: [PATCH 012/152] Change AttachmentProvider to use the new database structure --- .../com/fsck/k9/mailstore/LocalStore.java | 74 ++++++++++++++- .../fsck/k9/provider/AttachmentProvider.java | 92 +++++++++++-------- 2 files changed, 125 insertions(+), 41 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java index b12e0a936..a33b44bc0 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -31,8 +31,15 @@ import com.fsck.k9.search.LocalSearch; import com.fsck.k9.search.SearchSpecification.Attribute; import com.fsck.k9.search.SearchSpecification.Searchfield; import com.fsck.k9.search.SqlQueryBuilder; +import org.apache.james.mime4j.codec.Base64InputStream; +import org.apache.james.mime4j.codec.QuotedPrintableInputStream; +import org.apache.james.mime4j.util.MimeUtil; +import java.io.ByteArrayInputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; @@ -648,7 +655,7 @@ public class LocalStore extends Store implements Serializable { return null; } String name = cursor.getString(0); - int size = cursor.getInt(1); + long size = cursor.getLong(1); String mimeType = cursor.getString(2); final AttachmentInfo attachmentInfo = new AttachmentInfo(); @@ -664,9 +671,72 @@ public class LocalStore extends Store implements Serializable { }); } + public InputStream getAttachmentInputStream(final String attachmentId) throws MessagingException { + return database.execute(false, new DbCallback() { + @Override + public InputStream doDbWork(final SQLiteDatabase db) throws WrappedException { + Cursor cursor = db.query("message_parts", + new String[] { "data_location", "data", "encoding" }, + "id = ?", + new String[] { attachmentId }, + null, null, null); + try { + if (!cursor.moveToFirst()) { + return null; + } + + int location = cursor.getInt(0); + String encoding = cursor.getString(2); + + InputStream rawInputStream = getRawAttachmentInputStream(cursor, location, attachmentId); + return getDecodingInputStream(rawInputStream, encoding); + } finally { + cursor.close(); + } + } + }); + } + + private InputStream getRawAttachmentInputStream(Cursor cursor, int location, String attachmentId) { + switch (location) { + case DataLocation.IN_DATABASE: { + byte[] data = cursor.getBlob(1); + return new ByteArrayInputStream(data); + } + case DataLocation.ON_DISK: { + File file = getAttachmentFile(attachmentId); + try { + return new FileInputStream(file); + } catch (FileNotFoundException e) { + throw new WrappedException(e); + } + } + default: { + throw new IllegalStateException("No attachment data available"); + } + } + } + + private InputStream getDecodingInputStream(InputStream rawInputStream, String encoding) { + if (MimeUtil.ENC_BASE64.equals(encoding)) { + return new Base64InputStream(rawInputStream); + } + if (MimeUtil.ENC_QUOTED_PRINTABLE.equals(encoding)) { + return new QuotedPrintableInputStream(rawInputStream); + } + + return rawInputStream; + } + + private File getAttachmentFile(String attachmentId) { + final StorageManager storageManager = StorageManager.getInstance(context); + final File attachmentDirectory = storageManager.getAttachmentDirectory(uUid, database.getStorageProviderId()); + return new File(attachmentDirectory, attachmentId); + } + public static class AttachmentInfo { public String name; - public int size; //FIXME: use long + public long size; public String type; } diff --git a/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java b/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java index 75fdcef35..579861b6b 100644 --- a/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java +++ b/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java @@ -19,7 +19,7 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.LocalStore; import com.fsck.k9.mailstore.LocalStore.AttachmentInfo; -import com.fsck.k9.mailstore.StorageManager; +import org.openintents.openpgp.util.ParcelFileDescriptorUtil; import java.io.*; import java.util.List; @@ -156,8 +156,6 @@ public class AttachmentProvider extends ContentProvider { @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { - File file; - List segments = uri.getPathSegments(); String accountUuid = segments.get(0); String attachmentId = segments.get(1); @@ -167,36 +165,10 @@ public class AttachmentProvider extends ContentProvider { int width = Integer.parseInt(segments.get(3)); int height = Integer.parseInt(segments.get(4)); - file = getThumbnailFile(getContext(), accountUuid, attachmentId); - if (!file.exists()) { - String type = getType(accountUuid, attachmentId, FORMAT_VIEW, null); - try { - FileInputStream in = new FileInputStream(getFile(accountUuid, attachmentId)); - try { - Bitmap thumbnail = createThumbnail(type, in); - if (thumbnail != null) { - thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true); - FileOutputStream out = new FileOutputStream(file); - try { - thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out); - } finally { - out.close(); - } - } - } finally { - try { - in.close(); - } catch (Throwable ignore) { /* ignore */ } - } - } catch (IOException ioe) { - return null; - } - } - } else { - file = getFile(accountUuid, attachmentId); + return openThumbnail(accountUuid, attachmentId, width, height); } - return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + return openAttachment(accountUuid, attachmentId); } @Override @@ -282,18 +254,60 @@ public class AttachmentProvider extends ContentProvider { return type; } - private File getFile(String accountUuid, String id) throws FileNotFoundException { - Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid); + private ParcelFileDescriptor openThumbnail(String accountUuid, String attachmentId, int width, int height) + throws FileNotFoundException { - File attachmentsDir = StorageManager.getInstance(getContext()).getAttachmentDirectory(accountUuid, - account.getLocalStorageProviderId()); - - File file = new File(attachmentsDir, id); + File file = getThumbnailFile(getContext(), accountUuid, attachmentId); if (!file.exists()) { - throw new FileNotFoundException(file.getAbsolutePath()); + String type = getType(accountUuid, attachmentId, FORMAT_VIEW, null); + try { + InputStream in = getAttachmentInputStream(accountUuid, attachmentId); + try { + Bitmap thumbnail = createThumbnail(type, in); + if (thumbnail != null) { + thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true); + FileOutputStream out = new FileOutputStream(file); + try { + thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out); + } finally { + try { + out.close(); + } catch (IOException e) { + Log.e(K9.LOG_TAG, "Error saving thumbnail", e); + } + } + } + } finally { + try { + in.close(); + } catch (Throwable ignore) { /* ignore */ } + } + } catch (MessagingException e) { + Log.e(K9.LOG_TAG, "Error getting InputStream for attachment", e); + return null; + } } - return file; + return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + } + + private ParcelFileDescriptor openAttachment(String accountUuid, String attachmentId) { + try { + InputStream inputStream = getAttachmentInputStream(accountUuid, attachmentId); + return ParcelFileDescriptorUtil.pipeFrom(inputStream, null); + } catch (MessagingException e) { + Log.e(K9.LOG_TAG, "Error getting InputStream for attachment", e); + return null; + } catch (IOException e) { + Log.e(K9.LOG_TAG, "Error creating ParcelFileDescriptor", e); + return null; + } + } + + private InputStream getAttachmentInputStream(String accountUuid, String attachmentId) throws MessagingException { + final Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid); + LocalStore localStore = LocalStore.getInstance(account, getContext()); + return localStore.getAttachmentInputStream(attachmentId); } private Bitmap createThumbnail(String type, InputStream data) { From 743e640d8c71a0d4a6dd8e75cf760fa34c32f2c2 Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 10 Jan 2015 01:22:39 +0100 Subject: [PATCH 013/152] Remove references to 'text_content' and 'html_content' --- k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java | 2 -- k9mail/src/main/java/com/fsck/k9/provider/EmailProvider.java | 2 -- k9mail/src/main/java/com/fsck/k9/search/SqlQueryBuilder.java | 3 +-- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java index 7a0fa44cb..d377bc55d 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java @@ -308,8 +308,6 @@ public class LocalMessage extends MimeMessage { cv.putNull("cc_list"); cv.putNull("bcc_list"); cv.putNull("preview"); - cv.putNull("html_content"); - cv.putNull("text_content"); cv.putNull("reply_to_list"); cv.putNull("message_part_id"); diff --git a/k9mail/src/main/java/com/fsck/k9/provider/EmailProvider.java b/k9mail/src/main/java/com/fsck/k9/provider/EmailProvider.java index d599f4ac7..3ae32bd7c 100644 --- a/k9mail/src/main/java/com/fsck/k9/provider/EmailProvider.java +++ b/k9mail/src/main/java/com/fsck/k9/provider/EmailProvider.java @@ -152,8 +152,6 @@ public class EmailProvider extends ContentProvider { private interface InternalMessageColumns extends MessageColumns { public static final String DELETED = "deleted"; public static final String EMPTY = "empty"; - public static final String TEXT_CONTENT = "text_content"; - public static final String HTML_CONTENT = "html_content"; public static final String MIME_TYPE = "mime_type"; } diff --git a/k9mail/src/main/java/com/fsck/k9/search/SqlQueryBuilder.java b/k9mail/src/main/java/com/fsck/k9/search/SqlQueryBuilder.java index 118467301..5cbb18ff6 100644 --- a/k9mail/src/main/java/com/fsck/k9/search/SqlQueryBuilder.java +++ b/k9mail/src/main/java/com/fsck/k9/search/SqlQueryBuilder.java @@ -140,8 +140,7 @@ public class SqlQueryBuilder { break; } case MESSAGE_CONTENTS: { - columnName = "text_content"; - break; + throw new RuntimeException("Searching in message bodies is currently not supported"); } case REPLY_TO: { columnName = "reply_to_list"; From bd97004ebd16841d004d3ebe5f99a226fef648b8 Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 10 Jan 2015 04:04:37 +0100 Subject: [PATCH 014/152] Fix downloading/saving single message parts --- .../main/java/com/fsck/k9/mail/BodyPart.java | 17 ++++-- .../src/main/java/com/fsck/k9/mail/Part.java | 4 ++ .../com/fsck/k9/mail/internet/MimeHeader.java | 29 ++--------- .../fsck/k9/mail/internet/MimeMessage.java | 13 +++++ .../fsck/k9/mail/store/imap/ImapStore.java | 8 +-- .../com/fsck/k9/mailstore/LocalFolder.java | 52 ++++++++++++++++--- 6 files changed, 83 insertions(+), 40 deletions(-) diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/BodyPart.java b/k9mail-library/src/main/java/com/fsck/k9/mail/BodyPart.java index 551866829..ef48dce8f 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/BodyPart.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/BodyPart.java @@ -2,14 +2,25 @@ package com.fsck.k9.mail; public abstract class BodyPart implements Part { - private Multipart mParent; + private String serverExtra; + private Multipart parent; + + @Override + public String getServerExtra() { + return serverExtra; + } + + @Override + public void setServerExtra(String serverExtra) { + this.serverExtra = serverExtra; + } public Multipart getParent() { - return mParent; + return parent; } public void setParent(Multipart parent) { - mParent = parent; + this.parent = parent; } public abstract void setEncoding(String encoding) throws MessagingException; diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java index 15e371392..492b2a171 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java @@ -46,4 +46,8 @@ public interface Part { */ //TODO perhaps it would be clearer to use a flag "force7bit" in writeTo void setUsing7bitTransport() throws MessagingException; + + String getServerExtra(); + + void setServerExtra(String serverExtra); } diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java index f3835d04a..318ea6da0 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java @@ -11,28 +11,11 @@ import java.util.*; public class MimeHeader { private static final String[] EMPTY_STRING_ARRAY = new String[0]; - /** - * Application specific header that contains Store specific information about an attachment. - * In IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later - * retrieve the attachment at will from the server. - * The info is recorded from this header on LocalStore.appendMessages and is put back - * into the MIME data by LocalStore.fetch. - */ - public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData"; - public static final String HEADER_CONTENT_TYPE = "Content-Type"; public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding"; public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition"; public static final String HEADER_CONTENT_ID = "Content-ID"; - /** - * Fields that should be omitted when writing the header using writeTo() - */ - private static final String[] writeOmitFields = { -// HEADER_ANDROID_ATTACHMENT_DOWNLOADED, -// HEADER_ANDROID_ATTACHMENT_ID, - HEADER_ANDROID_ATTACHMENT_STORE_DATA - }; private List mFields = new ArrayList(); private String mCharset = null; @@ -101,14 +84,12 @@ public class MimeHeader { public void writeTo(OutputStream out) throws IOException { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); for (Field field : mFields) { - if (!Arrays.asList(writeOmitFields).contains(field.name)) { - if (field.hasRawData()) { - writer.write(field.getRaw()); - } else { - writeNameValueField(writer, field); - } - writer.write("\r\n"); + if (field.hasRawData()) { + writer.write(field.getRaw()); + } else { + writeNameValueField(writer, field); } + writer.write("\r\n"); } writer.flush(); } diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java index e7909c105..4cf09460e 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java @@ -57,6 +57,7 @@ public class MimeMessage extends Message { private Body mBody; protected int mSize; + private String serverExtra; public MimeMessage() { } @@ -693,4 +694,16 @@ public class MimeMessage extends Message { setEncoding(MimeUtil.ENC_QUOTED_PRINTABLE); } } + + @Override + public String getServerExtra() { + return serverExtra; + } + + @Override + public void setServerExtra(String serverExtra) { + this.serverExtra = serverExtra; + } + + } diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java b/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java index 7df04e2b4..6cf6825fe 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java @@ -1455,13 +1455,9 @@ public class ImapStore extends RemoteStore { throws MessagingException { checkOpen(); //only need READ access - String[] parts = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA); - if (parts == null) { - return; - } + String partId = part.getServerExtra(); String fetch; - String partId = parts[0]; if ("TEXT".equalsIgnoreCase(partId)) { fetch = String.format(Locale.US, "BODY.PEEK[TEXT]<0.%d>", mStoreConfig.getMaximumAutoDownloadMessageSize()); @@ -1834,7 +1830,7 @@ public class ImapStore extends RemoteStore { if (part instanceof ImapMessage) { ((ImapMessage) part).setSize(size); } - part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); + part.setServerExtra(id); } } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index a4eb27112..265a0015c 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -672,6 +672,7 @@ public class LocalFolder extends Folder implements Serializable { long parentId = cursor.getLong(2); String mimeType = cursor.getString(3); byte[] header = cursor.getBlob(6); + String serverExtra = cursor.getString(15); final Part part; if (id == message.getMessagePartId()) { @@ -698,6 +699,7 @@ public class LocalFolder extends Folder implements Serializable { parseHeaderBytes(part, header); } partById.put(id, part); + part.setServerExtra(serverExtra); boolean isMultipart = mimeType.startsWith("multipart/"); if (isMultipart) { @@ -1322,14 +1324,22 @@ public class LocalFolder extends Folder implements Serializable { Part part = partContainer.part; - byte[] headerBytes = getHeaderBytes(part); - ContentValues cv = new ContentValues(); if (rootMessagePartId != -1) { cv.put("root", rootMessagePartId); } cv.put("parent", partContainer.parent); cv.put("seq", order); + cv.put("server_extra", part.getServerExtra()); + + partToContentValues(cv, part); + + return db.insertOrThrow("message_parts", null, cv); + } + + private void partToContentValues(ContentValues cv, Part part) throws IOException, MessagingException { + byte[] headerBytes = getHeaderBytes(part); + cv.put("mime_type", part.getMimeType()); cv.put("header", headerBytes); cv.put("type", MessagePartType.UNKNOWN); @@ -1355,8 +1365,6 @@ public class LocalFolder extends Folder implements Serializable { cv.put("data", bodyData); cv.put("content_id", part.getContentId()); } - - return db.insertOrThrow("message_parts", null, cv); } private byte[] getHeaderBytes(Part part) throws IOException, MessagingException { @@ -1403,9 +1411,39 @@ public class LocalFolder extends Folder implements Serializable { public void addPartToMessage(final LocalMessage message, final Part part) throws MessagingException { open(OPEN_MODE_RW); - throw new RuntimeException("Not implemented yet"); -// localStore.notifyChange(); + localStore.database.execute(false, new DbCallback() { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { + String messagePartId; + + Cursor cursor = db.query("message_parts", new String[] { "id" }, "root = ? AND server_extra = ?", + new String[] { Long.toString(message.getMessagePartId()), part.getServerExtra() }, + null, null, null); + try { + if (!cursor.moveToFirst()) { + throw new IllegalStateException("Message part not found"); + } + + messagePartId = cursor.getString(0); + } finally { + cursor.close(); + } + + try { + ContentValues cv = new ContentValues(); + partToContentValues(cv, part); + + db.update("message_parts", cv, "id = ?", new String[] { messagePartId }); + } catch (Exception e) { + Log.e(K9.LOG_TAG, "Error writing message part", e); + } + + return null; + } + }); + + localStore.notifyChange(); } /** @@ -1422,7 +1460,7 @@ public class LocalFolder extends Folder implements Serializable { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { db.update("messages", cv, "id = ?", new String[] - { Long.toString(message.getId()) }); + { Long.toString(message.getId()) }); return null; } }); From 2532362ed5324b8c31f1ecf3e102d36295d5e551 Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 10 Jan 2015 04:31:43 +0100 Subject: [PATCH 015/152] Add test for updating a message with a missing part --- .../ReconstructMessageFromDatabaseTest.java | 35 +++++++++++++++++++ .../com/fsck/k9/mailstore/LocalFolder.java | 3 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/ReconstructMessageFromDatabaseTest.java b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/ReconstructMessageFromDatabaseTest.java index a4b1aaaee..3b2c272e4 100644 --- a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/ReconstructMessageFromDatabaseTest.java +++ b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/ReconstructMessageFromDatabaseTest.java @@ -13,10 +13,12 @@ import android.test.RenamingDelegatingContext; import com.fsck.k9.Account; import com.fsck.k9.K9; +import com.fsck.k9.mail.Body; import com.fsck.k9.mail.FetchProfile; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.BinaryTempFileBody; import com.fsck.k9.mail.internet.MimeMessage; +import org.apache.james.mime4j.util.MimeUtil; public class ReconstructMessageFromDatabaseTest extends ApplicationTestCase { @@ -89,6 +91,39 @@ public class ReconstructMessageFromDatabaseTest extends ApplicationTestCase assertEquals(MESSAGE_SOURCE, reconstructedMessage); } + public void testAddMissingPart() throws MessagingException, IOException { + LocalFolder folder = createFolderInDatabase(); + + MimeMessage message = new MimeMessage(); + message.addHeader("To", "to@example.com"); + message.addHeader("MIME-Version", "1.0"); + message.addHeader("Content-Type", "text/plain"); + message.setServerExtra("text"); + + saveMessageToDatabase(folder, message); + + LocalMessage localMessage = readMessageFromDatabase(folder, message); + + assertEquals("to@example.com", localMessage.getHeader("To")[0]); + assertEquals("text/plain", localMessage.getMimeType()); + assertEquals("text", localMessage.getServerExtra()); + assertNull(localMessage.getBody()); + + Body body = new BinaryMemoryBody("Test message body".getBytes(), MimeUtil.ENC_7BIT); + localMessage.setBody(body); + folder.addPartToMessage(localMessage, localMessage); + + LocalMessage completeLocalMessage = readMessageFromDatabase(folder, message); + String reconstructedMessage = writeMessageToString(completeLocalMessage); + + assertEquals("To: to@example.com\r\n" + + "MIME-Version: 1.0\r\n" + + "Content-Type: text/plain\r\n" + + "\r\n" + + "Test message body", + reconstructedMessage); + } + protected MimeMessage parseMessage() throws IOException, MessagingException { InputStream messageInputStream = new ByteArrayInputStream(MESSAGE_SOURCE.getBytes()); try { diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 265a0015c..d15fdec2e 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -672,6 +672,7 @@ public class LocalFolder extends Folder implements Serializable { long parentId = cursor.getLong(2); String mimeType = cursor.getString(3); byte[] header = cursor.getBlob(6); + int dataLocation = cursor.getInt(9); String serverExtra = cursor.getString(15); final Part part; @@ -711,7 +712,7 @@ public class LocalFolder extends Folder implements Serializable { part.setBody(multipart); multipart.setPreamble(preamble); multipart.setEpilogue(epilogue); - } else { + } else if (dataLocation != DataLocation.MISSING) { String encoding = cursor.getString(7); byte[] data = cursor.getBlob(10); From bcd64017e3ac9ba252fc6da7a6f3aed411696fb5 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 12 Jan 2015 21:40:23 +0100 Subject: [PATCH 016/152] Extract text to display before viewing the message --- .../java/com/fsck/k9/mailstore/LocalMessageExtractor.java | 2 +- .../main/java/com/fsck/k9/mailstore/ViewableContainer.java | 2 +- .../src/main/java/com/fsck/k9/view/SingleMessageView.java | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java index 864b9132f..229a166fe 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java @@ -25,7 +25,7 @@ import static com.fsck.k9.mail.internet.Viewable.MessageHeader; import static com.fsck.k9.mail.internet.Viewable.Text; import static com.fsck.k9.mail.internet.Viewable.Textual; -class LocalMessageExtractor { +public class LocalMessageExtractor { private static final String TEXT_DIVIDER = "------------------------------------------------------------------------"; private static final int TEXT_DIVIDER_LENGTH = TEXT_DIVIDER.length(); diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/ViewableContainer.java b/k9mail/src/main/java/com/fsck/k9/mailstore/ViewableContainer.java index 2e538cd22..64e68527e 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/ViewableContainer.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/ViewableContainer.java @@ -10,7 +10,7 @@ import java.util.List; * * @see LocalMessageExtractor#extractTextAndAttachments(android.content.Context, com.fsck.k9.mail.Message) */ -class ViewableContainer { +public class ViewableContainer { /** * The viewable text of the message in plain text. */ diff --git a/k9mail/src/main/java/com/fsck/k9/view/SingleMessageView.java b/k9mail/src/main/java/com/fsck/k9/view/SingleMessageView.java index 25852ee17..289256a31 100644 --- a/k9mail/src/main/java/com/fsck/k9/view/SingleMessageView.java +++ b/k9mail/src/main/java/com/fsck/k9/view/SingleMessageView.java @@ -58,6 +58,8 @@ import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.LocalAttachmentBodyPart; import com.fsck.k9.mailstore.LocalMessage; +import com.fsck.k9.mailstore.LocalMessageExtractor; +import com.fsck.k9.mailstore.ViewableContainer; import com.fsck.k9.provider.AttachmentProvider.AttachmentProviderColumns; import org.apache.commons.io.IOUtils; @@ -512,7 +514,9 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, } if (text == null) { - text = message.getTextForDisplay(); + //FIXME: Run the text extraction in a background thread because it might involve disk I/O + ViewableContainer viewables = LocalMessageExtractor.extractTextAndAttachments(getContext(), message); + text = viewables.html; } // Save the text so we can reset the WebView when the user clicks the "Show pictures" button From 1bf159a3005a605d2a72ed5cc4af336269e7db8d Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 12 Jan 2015 22:09:55 +0100 Subject: [PATCH 017/152] Get rid of unused LocalTextBody --- .../fsck/k9/mailstore/LocalMessageTest.java | 44 ------------------- .../com/fsck/k9/mailstore/LocalMessage.java | 23 ---------- .../com/fsck/k9/mailstore/LocalTextBody.java | 20 --------- 3 files changed, 87 deletions(-) delete mode 100644 k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageTest.java delete mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/LocalTextBody.java diff --git a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageTest.java b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageTest.java deleted file mode 100644 index 01b4d8e22..000000000 --- a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.fsck.k9.mailstore; - - -import android.test.AndroidTestCase; - -import com.fsck.k9.Preferences; -import com.fsck.k9.mail.internet.MimeBodyPart; -import com.fsck.k9.mail.internet.MimeMultipart; - - -public class LocalMessageTest extends AndroidTestCase { - private LocalMessage message; - - @Override - public void setUp() throws Exception { - super.setUp(); - Preferences preferences = Preferences.getPreferences(getContext()); - LocalStore store = LocalStore.getInstance(preferences.newAccount(), getContext()); - message = new LocalMessage(store, "uid", new LocalFolder(store, "test")); - } - - public void testGetDisplayTextWithPlainTextPart() throws Exception { - String textBodyText = "text body"; - - MimeMultipart multipart = new MimeMultipart(); - MimeBodyPart bodyPart1 = new MimeBodyPart(new LocalTextBody(textBodyText, textBodyText), "text/plain"); - multipart.addBodyPart(bodyPart1); - message.setBody(multipart); - assertEquals("text body", message.getTextForDisplay()); - } - - public void testGetDisplayTextWithHtmlPart() throws Exception { - String htmlBodyText = "html body"; - String textBodyText = "text body"; - - MimeMultipart multipart = new MimeMultipart(); - MimeBodyPart bodyPart1 = new MimeBodyPart(new LocalTextBody(htmlBodyText, htmlBodyText), "text/html"); - MimeBodyPart bodyPart2 = new MimeBodyPart(new LocalTextBody(textBodyText, textBodyText), "text/plain"); - multipart.addBodyPart(bodyPart1); - multipart.addBodyPart(bodyPart2); - message.setBody(multipart); - assertEquals("html body", message.getTextForDisplay()); - } -} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java index d377bc55d..de19dc123 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java @@ -124,29 +124,6 @@ public class LocalMessage extends MimeMessage { return mimeType; } - /** - * Fetch the message text for display. This always returns an HTML-ified version of the - * message, even if it was originally a text-only message. - * @return HTML version of message for display purposes or null. - * @throws MessagingException - */ - public String getTextForDisplay() throws MessagingException { - String text = null; // First try and fetch an HTML part. - Part part = MimeUtility.findFirstPartByMimeType(this, "text/html"); - if (part == null) { - // If that fails, try and get a text part. - part = MimeUtility.findFirstPartByMimeType(this, "text/plain"); - if (part != null && part.getBody() instanceof LocalTextBody) { - text = ((LocalTextBody) part.getBody()).getBodyForDisplay(); - } - } else { - // We successfully found an HTML part; do the necessary character set decoding. - text = MessageExtractor.getTextFromPart(part); - } - return text; - } - - /* Custom version of writeTo that updates the MIME message based on localMessage * changes. */ diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalTextBody.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalTextBody.java deleted file mode 100644 index 57c4963cf..000000000 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalTextBody.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.fsck.k9.mailstore; - -import com.fsck.k9.mail.internet.TextBody; - -class LocalTextBody extends TextBody { - /** - * This is an HTML-ified version of the message for display purposes. - */ - private final String mBodyForDisplay; - - public LocalTextBody(String body, String bodyForDisplay) { - super(body); - this.mBodyForDisplay = bodyForDisplay; - } - - public String getBodyForDisplay() { - return mBodyForDisplay; - } - -}//LocalTextBody From 787c014265158d249443802d38ff1207ac9ffa5d Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 12 Jan 2015 22:46:56 +0100 Subject: [PATCH 018/152] Create new package for UI code related to message viewing --- .../main/java/com/fsck/k9/activity/MessageList.java | 6 +++--- .../{view => ui/messageview}/MessageOpenPgpView.java | 3 +-- .../messageview}/MessageViewFragment.java | 5 +++-- .../{view => ui/messageview}/SingleMessageView.java | 12 ++++++++---- k9mail/src/main/res/layout/message.xml | 4 ++-- .../main/res/layout/message_view_openpgp_layout.xml | 4 ++-- 6 files changed, 19 insertions(+), 15 deletions(-) rename k9mail/src/main/java/com/fsck/k9/{view => ui/messageview}/MessageOpenPgpView.java (99%) rename k9mail/src/main/java/com/fsck/k9/{fragment => ui/messageview}/MessageViewFragment.java (99%) rename k9mail/src/main/java/com/fsck/k9/{view => ui/messageview}/SingleMessageView.java (98%) diff --git a/k9mail/src/main/java/com/fsck/k9/activity/MessageList.java b/k9mail/src/main/java/com/fsck/k9/activity/MessageList.java index 2d75c1633..a1a817863 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/MessageList.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/MessageList.java @@ -40,8 +40,8 @@ import com.fsck.k9.activity.setup.Prefs; import com.fsck.k9.crypto.PgpData; import com.fsck.k9.fragment.MessageListFragment; import com.fsck.k9.fragment.MessageListFragment.MessageListFragmentListener; -import com.fsck.k9.fragment.MessageViewFragment; -import com.fsck.k9.fragment.MessageViewFragment.MessageViewFragmentListener; +import com.fsck.k9.ui.messageview.MessageViewFragment; +import com.fsck.k9.ui.messageview.MessageViewFragment.MessageViewFragmentListener; import com.fsck.k9.mailstore.StorageManager; import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.search.LocalSearch; @@ -51,7 +51,7 @@ import com.fsck.k9.search.SearchSpecification.Attribute; import com.fsck.k9.search.SearchSpecification.SearchCondition; import com.fsck.k9.search.SearchSpecification.Searchfield; import com.fsck.k9.view.MessageHeader; -import com.fsck.k9.view.MessageOpenPgpView; +import com.fsck.k9.ui.messageview.MessageOpenPgpView; import com.fsck.k9.view.MessageTitleView; import com.fsck.k9.view.ViewSwitcher; import com.fsck.k9.view.ViewSwitcher.OnSwitchCompleteListener; diff --git a/k9mail/src/main/java/com/fsck/k9/view/MessageOpenPgpView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageOpenPgpView.java similarity index 99% rename from k9mail/src/main/java/com/fsck/k9/view/MessageOpenPgpView.java rename to k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageOpenPgpView.java index 3c0e857da..8cddbe7e4 100644 --- a/k9mail/src/main/java/com/fsck/k9/view/MessageOpenPgpView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageOpenPgpView.java @@ -1,5 +1,5 @@ -package com.fsck.k9.view; +package com.fsck.k9.ui.messageview; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -30,7 +30,6 @@ import com.fsck.k9.K9; import com.fsck.k9.R; import com.fsck.k9.crypto.CryptoHelper; import com.fsck.k9.crypto.OpenPgpApiHelper; -import com.fsck.k9.fragment.MessageViewFragment; import com.fsck.k9.helper.IdentityHelper; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; diff --git a/k9mail/src/main/java/com/fsck/k9/fragment/MessageViewFragment.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java similarity index 99% rename from k9mail/src/main/java/com/fsck/k9/fragment/MessageViewFragment.java rename to k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java index 961a38555..a93b96a8d 100644 --- a/k9mail/src/main/java/com/fsck/k9/fragment/MessageViewFragment.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java @@ -1,4 +1,4 @@ -package com.fsck.k9.fragment; +package com.fsck.k9.ui.messageview; import java.io.File; import java.util.Collections; @@ -31,7 +31,9 @@ import com.fsck.k9.activity.MessageReference; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.crypto.PgpData; +import com.fsck.k9.fragment.ConfirmationDialogFragment; import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener; +import com.fsck.k9.fragment.ProgressDialogFragment; import com.fsck.k9.helper.FileBrowserHelper; import com.fsck.k9.helper.FileBrowserHelper.FileBrowserFailOverCallback; import com.fsck.k9.mail.Flag; @@ -42,7 +44,6 @@ import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.view.AttachmentView; import com.fsck.k9.view.AttachmentView.AttachmentFileDownloadCallback; import com.fsck.k9.view.MessageHeader; -import com.fsck.k9.view.SingleMessageView; import org.openintents.openpgp.OpenPgpSignatureResult; diff --git a/k9mail/src/main/java/com/fsck/k9/view/SingleMessageView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java similarity index 98% rename from k9mail/src/main/java/com/fsck/k9/view/SingleMessageView.java rename to k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java index 289256a31..d22662ef9 100644 --- a/k9mail/src/main/java/com/fsck/k9/view/SingleMessageView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java @@ -1,4 +1,4 @@ -package com.fsck.k9.view; +package com.fsck.k9.ui.messageview; import java.io.File; import java.io.FileOutputStream; @@ -42,7 +42,6 @@ import com.fsck.k9.R; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.crypto.PgpData; -import com.fsck.k9.fragment.MessageViewFragment; import com.fsck.k9.helper.ClipboardManager; import com.fsck.k9.helper.Contacts; import com.fsck.k9.helper.FileHelper; @@ -62,11 +61,16 @@ import com.fsck.k9.mailstore.LocalMessageExtractor; import com.fsck.k9.mailstore.ViewableContainer; import com.fsck.k9.provider.AttachmentProvider.AttachmentProviderColumns; +import com.fsck.k9.view.AttachmentView; +import com.fsck.k9.view.AttachmentView.AttachmentFileDownloadCallback; +import com.fsck.k9.view.MessageHeader; +import com.fsck.k9.view.MessageHeader.OnLayoutChangedListener; +import com.fsck.k9.view.MessageWebView; import org.apache.commons.io.IOUtils; public class SingleMessageView extends LinearLayout implements OnClickListener, - MessageHeader.OnLayoutChangedListener, OnCreateContextMenuListener { + OnLayoutChangedListener, OnCreateContextMenuListener { private static final int MENU_ITEM_LINK_VIEW = Menu.FIRST; private static final int MENU_ITEM_LINK_SHARE = Menu.FIRST + 1; private static final int MENU_ITEM_LINK_COPY = Menu.FIRST + 2; @@ -104,7 +108,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, private Button mDownloadRemainder; private LayoutInflater mInflater; private Contacts mContacts; - private AttachmentView.AttachmentFileDownloadCallback attachmentCallback; + private AttachmentFileDownloadCallback attachmentCallback; private View mAttachmentsContainer; private SavedState mSavedState; private ClipboardManager mClipboardManager; diff --git a/k9mail/src/main/res/layout/message.xml b/k9mail/src/main/res/layout/message.xml index a054bc2bd..cbf2552de 100644 --- a/k9mail/src/main/res/layout/message.xml +++ b/k9mail/src/main/res/layout/message.xml @@ -1,5 +1,5 @@ - - + diff --git a/k9mail/src/main/res/layout/message_view_openpgp_layout.xml b/k9mail/src/main/res/layout/message_view_openpgp_layout.xml index 6f87b6d46..6d4a2ca06 100644 --- a/k9mail/src/main/res/layout/message_view_openpgp_layout.xml +++ b/k9mail/src/main/res/layout/message_view_openpgp_layout.xml @@ -1,5 +1,5 @@ - - + From 78ed2a23b126cde1a06d072c79858b19ef0cde36 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 13 Jan 2015 04:17:25 +0100 Subject: [PATCH 019/152] Use a Loader to load the message to view from the database --- .../k9/controller/MessagingController.java | 39 ++-- .../com/fsck/k9/mailstore/LocalMessage.java | 4 + .../k9/ui/message/LocalMessageLoader.java | 60 +++++ .../ui/messageview/MessageViewFragment.java | 218 ++++++++---------- .../k9/ui/messageview/SingleMessageView.java | 7 +- .../java/com/fsck/k9/view/MessageHeader.java | 2 - 6 files changed, 179 insertions(+), 151 deletions(-) create mode 100644 k9mail/src/main/java/com/fsck/k9/ui/message/LocalMessageLoader.java diff --git a/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java b/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java index 95c38f10e..8b0cd1495 100644 --- a/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -3113,27 +3113,34 @@ public class MessagingController implements Runnable { }); } - /** - * Mark the provided message as read if not disabled by the account setting. - * - * @param account - * The account the message belongs to. - * @param message - * The message to mark as read. This {@link Message} instance will be modify by calling - * {@link Message#setFlag(Flag, boolean)} on it. - * - * @throws MessagingException - * - * @see Account#isMarkMessageAsReadOnView() - */ - private void markMessageAsReadOnView(Account account, Message message) + public LocalMessage loadMessage(Account account, String folderName, String uid) throws MessagingException { + LocalStore localStore = account.getLocalStore(); + LocalFolder localFolder = localStore.getFolder(folderName); + localFolder.open(Folder.OPEN_MODE_RW); + + LocalMessage message = localFolder.getMessage(uid); + if (message == null || message.getId() == 0) { + throw new IllegalArgumentException("Message not found: folder=" + folderName + ", uid=" + uid); + } + + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.BODY); + localFolder.fetch(Collections.singletonList(message), fp, null); + localFolder.close(); + + markMessageAsReadOnView(account, message); + + return message; + } + + private void markMessageAsReadOnView(Account account, LocalMessage message) throws MessagingException { if (account.isMarkMessageAsReadOnView() && !message.isSet(Flag.SEEN)) { List messageIds = Collections.singletonList(message.getId()); setFlag(account, messageIds, Flag.SEEN, true); - ((LocalMessage) message).setFlagInternal(Flag.SEEN, true); + message.setFlagInternal(Flag.SEEN, true); } } @@ -4013,7 +4020,7 @@ public class MessagingController implements Runnable { @Override public void act(final Account account, final Folder folder, - final List accountMessages) { + final List accountMessages) { suppressMessages(account, messages); putBackground("deleteMessages", null, new Runnable() { diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java index de19dc123..b8fd2c1ec 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java @@ -554,4 +554,8 @@ public class LocalMessage extends MimeMessage { private String getAccountUuid() { return getAccount().getUuid(); } + + public boolean isBodyMissing() { + return getBody() == null; + } } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/message/LocalMessageLoader.java b/k9mail/src/main/java/com/fsck/k9/ui/message/LocalMessageLoader.java new file mode 100644 index 000000000..de897f402 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/ui/message/LocalMessageLoader.java @@ -0,0 +1,60 @@ +package com.fsck.k9.ui.message; + + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.util.Log; + +import com.fsck.k9.Account; +import com.fsck.k9.K9; +import com.fsck.k9.activity.MessageReference; +import com.fsck.k9.controller.MessagingController; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mailstore.LocalMessage; + + +public class LocalMessageLoader extends AsyncTaskLoader { + private final MessagingController controller; + private final Account account; + private final MessageReference messageReference; + private LocalMessage message; + + public LocalMessageLoader(Context context, MessagingController controller, Account account, + MessageReference messageReference) { + super(context); + this.controller = controller; + this.account = account; + this.messageReference = messageReference; + } + + @Override + protected void onStartLoading() { + if (message != null) { + super.deliverResult(message); + } + + if (takeContentChanged() || message == null) { + forceLoad(); + } + } + + @Override + public void deliverResult(LocalMessage message) { + this.message = message; + super.deliverResult(message); + } + + @Override + public LocalMessage loadInBackground() { + try { + return loadMessageFromDatabase(); + } catch (Exception e) { + Log.e(K9.LOG_TAG, "Error while loading message from database", e); + return null; + } + } + + private LocalMessage loadMessageFromDatabase() throws MessagingException { + return controller.loadMessage(account, messageReference.folderName, messageReference.uid); + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java index a93b96a8d..47740d65f 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java @@ -6,13 +6,16 @@ import java.util.Locale; import android.app.Activity; import android.app.Fragment; +import android.app.LoaderManager.LoaderCallbacks; import android.content.Context; import android.content.Intent; +import android.content.Loader; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.app.DialogFragment; import android.app.FragmentManager; +import android.text.TextUtils; import android.util.Log; import android.view.ContextThemeWrapper; import android.view.KeyEvent; @@ -41,6 +44,7 @@ import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.mailstore.LocalMessage; +import com.fsck.k9.ui.message.LocalMessageLoader; import com.fsck.k9.view.AttachmentView; import com.fsck.k9.view.AttachmentView.AttachmentFileDownloadCallback; import com.fsck.k9.view.MessageHeader; @@ -60,6 +64,8 @@ public class MessageViewFragment extends Fragment implements OnClickListener, private static final int ACTIVITY_CHOOSE_FOLDER_COPY = 2; private static final int ACTIVITY_CHOOSE_DIRECTORY = 3; + private static final int LOCAL_MESSAGE_LOADER_ID = 1; + public static MessageViewFragment newInstance(MessageReference reference) { MessageViewFragment fragment = new MessageViewFragment(); @@ -105,6 +111,8 @@ public class MessageViewFragment extends Fragment implements OnClickListener, private Context mContext; + private LoaderCallbacks localMessageLoaderCallback = new LocalMessageLoaderCallback(); + class MessageViewHandler extends Handler { @@ -117,15 +125,6 @@ public class MessageViewFragment extends Fragment implements OnClickListener, }); } - public void addAttachment(final View attachmentView) { - post(new Runnable() { - @Override - public void run() { - mMessageView.addAttachment(attachmentView); - } - }); - } - /* A helper for a set of "show a toast" methods */ private void showToast(final String message, final int toastLength) { post(new Runnable() { @@ -146,16 +145,6 @@ public class MessageViewFragment extends Fragment implements OnClickListener, showToast(context.getString(R.string.status_network_error), Toast.LENGTH_LONG); } - public void invalidIdError() { - Context context = getActivity(); - if (context == null) { - return; - } - - showToast(context.getString(R.string.status_invalid_id_error), Toast.LENGTH_LONG); - } - - public void fetchingAttachment() { Context context = getActivity(); if (context == null) { @@ -230,7 +219,12 @@ public class MessageViewFragment extends Fragment implements OnClickListener, }; }); - mMessageView.initialize(this); + mMessageView.initialize(this, new OnClickListener() { + @Override + public void onClick(View v) { + onToggleFlagged(); + } + }); mMessageView.downloadRemainderButton().setOnClickListener(this); mFragmentListener.messageHeaderViewAvailable(mMessageView.getMessageHeaderView()); @@ -261,10 +255,6 @@ public class MessageViewFragment extends Fragment implements OnClickListener, outState.putSerializable(STATE_PGP_DATA, mPgpData); } - public void displayMessage(MessageReference ref) { - displayMessage(ref, true); - } - private void displayMessage(MessageReference ref, boolean resetPgpData) { mMessageReference = ref; if (K9.DEBUG) { @@ -283,11 +273,50 @@ public class MessageViewFragment extends Fragment implements OnClickListener, mMessageView.resetView(); mMessageView.resetHeaderView(); - mController.loadMessageForView(mAccount, mMessageReference.folderName, mMessageReference.uid, mListener); + startLoadingMessageFromDatabase(); mFragmentListener.updateMenu(); } + private void startLoadingMessageFromDatabase() { + getLoaderManager().initLoader(LOCAL_MESSAGE_LOADER_ID, null, localMessageLoaderCallback); + } + + private void onLoadMessageFromDatabaseFinished(LocalMessage message) { + displayMessageHeader(message); + + if (message.isBodyMissing()) { + startDownloadingMessageBody(message); + } else { + startExtractingTextAndAttachments(message); + } + } + + private void onLoadMessageFromDatabaseFailed() { + mMessageView.showStatusMessage(mContext.getString(R.string.status_invalid_id_error)); + } + + private void startDownloadingMessageBody(LocalMessage message) { + throw new RuntimeException("Not implemented yet"); + } + + private void startExtractingTextAndAttachments(LocalMessage message) { + //TODO: extract in background thread + //TODO: handle decryption and signature verification + try { + mMessageView.setMessage(mAccount, message, mPgpData, mController, mListener); + mMessageView.setShowDownloadButton(message); + } catch (MessagingException e) { + Log.e(K9.LOG_TAG, "Error while trying to display message", e); + } + } + + private void displayMessageHeader(LocalMessage message) { + mMessageView.setHeaders(message, mAccount); + displayMessageSubject(getSubjectForMessage(message)); + mFragmentListener.updateMenu(); + } + /** * Called from UI thread when user select Delete */ @@ -516,6 +545,15 @@ public class MessageViewFragment extends Fragment implements OnClickListener, } } + private String getSubjectForMessage(LocalMessage message) { + String subject = message.getSubject(); + if (TextUtils.isEmpty(subject)) { + return mContext.getString(R.string.general_no_subject); + } + + return subject; + } + public void moveMessage(MessageReference reference, String destFolderName) { mController.moveMessage(mAccount, mMessageReference.folderName, mMessage, destFolderName, null); @@ -530,126 +568,28 @@ public class MessageViewFragment extends Fragment implements OnClickListener, @Override public void loadMessageForViewHeadersAvailable(final Account account, String folder, String uid, final Message message) { - if (!mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder) - || !mMessageReference.accountUuid.equals(account.getUuid())) { - return; - } - - /* - * Clone the message object because the original could be modified by - * MessagingController later. This could lead to a ConcurrentModificationException - * when that same object is accessed by the UI thread (below). - * - * See issue 3953 - * - * This is just an ugly hack to get rid of the most pressing problem. A proper way to - * fix this is to make Message thread-safe. Or, even better, rewriting the UI code to - * access messages via a ContentProvider. - * - */ - final Message clonedMessage = message.clone(); - - mHandler.post(new Runnable() { - @Override - public void run() { - if (!clonedMessage.isSet(Flag.X_DOWNLOADED_FULL) && - !clonedMessage.isSet(Flag.X_DOWNLOADED_PARTIAL)) { - String text = mContext.getString(R.string.message_view_downloading); - mMessageView.showStatusMessage(text); - } - mMessageView.setHeaders(clonedMessage, account); - final String subject = clonedMessage.getSubject(); - if (subject == null || subject.equals("")) { - displayMessageSubject(mContext.getString(R.string.general_no_subject)); - } else { - displayMessageSubject(clonedMessage.getSubject()); - } - mMessageView.setOnFlagListener(new OnClickListener() { - @Override - public void onClick(View v) { - onToggleFlagged(); - } - }); - } - }); + throw new IllegalStateException(); } @Override public void loadMessageForViewBodyAvailable(final Account account, String folder, String uid, final Message message) { - if (!(message instanceof LocalMessage) || - !mMessageReference.uid.equals(uid) || - !mMessageReference.folderName.equals(folder) || - !mMessageReference.accountUuid.equals(account.getUuid())) { - return; - } - - mHandler.post(new Runnable() { - @Override - public void run() { - try { - mMessage = (LocalMessage) message; - mMessageView.setMessage(account, (LocalMessage) message, mPgpData, - mController, mListener); - mFragmentListener.updateMenu(); - - } catch (MessagingException e) { - Log.v(K9.LOG_TAG, "loadMessageForViewBodyAvailable", e); - } - } - }); + throw new IllegalStateException(); } @Override public void loadMessageForViewFailed(Account account, String folder, String uid, final Throwable t) { - if (!mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder) - || !mMessageReference.accountUuid.equals(account.getUuid())) { - return; - } - mHandler.post(new Runnable() { - @Override - public void run() { - setProgress(false); - if (t instanceof IllegalArgumentException) { - mHandler.invalidIdError(); - } else { - mHandler.networkError(); - } - if (mMessage == null || mMessage.isSet(Flag.X_DOWNLOADED_PARTIAL)) { - mMessageView.showStatusMessage( - mContext.getString(R.string.webview_empty_message)); - } - } - }); + throw new IllegalStateException(); } @Override public void loadMessageForViewFinished(Account account, String folder, String uid, final Message message) { - if (!mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder) - || !mMessageReference.accountUuid.equals(account.getUuid())) { - return; - } - mHandler.post(new Runnable() { - @Override - public void run() { - setProgress(false); - mMessageView.setShowDownloadButton(message); - } - }); + throw new IllegalStateException(); } @Override public void loadMessageForViewStarted(Account account, String folder, String uid) { - if (!mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder) - || !mMessageReference.accountUuid.equals(account.getUuid())) { - return; - } - mHandler.post(new Runnable() { - @Override - public void run() { - setProgress(true); - } - }); + throw new IllegalStateException(); } @Override @@ -865,4 +805,26 @@ public class MessageViewFragment extends Fragment implements OnClickListener, public LayoutInflater getFragmentLayoutInflater() { return mLayoutInflater; } + + class LocalMessageLoaderCallback implements LoaderCallbacks { + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new LocalMessageLoader(mContext, mController, mAccount, mMessageReference); + } + + @Override + public void onLoadFinished(Loader loader, LocalMessage message) { + mMessage = message; + if (message == null) { + onLoadMessageFromDatabaseFailed(); + } else { + onLoadMessageFromDatabaseFinished(message); + } + } + + @Override + public void onLoaderReset(Loader loader) { + // Do nothing + } + } } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java index d22662ef9..fbe7d01f4 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java @@ -115,7 +115,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, private String mText; - public void initialize(Fragment fragment) { + public void initialize(Fragment fragment, OnClickListener flagListener) { Activity activity = fragment.getActivity(); mMessageContentView = (MessageWebView) findViewById(R.id.message_content); mMessageContentView.configure(); @@ -124,6 +124,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, mHeaderContainer = (MessageHeader) findViewById(R.id.header_container); mHeaderContainer.setOnLayoutChangedListener(this); + mHeaderContainer.setOnFlagListener(flagListener); mAttachmentsContainer = findViewById(R.id.attachments_container); mAttachments = (LinearLayout) findViewById(R.id.attachments); @@ -493,10 +494,6 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, } } - public void setOnFlagListener(OnClickListener listener) { - mHeaderContainer.setOnFlagListener(listener); - } - public void showAllHeaders() { mHeaderContainer.onShowAdditionalHeaders(); } diff --git a/k9mail/src/main/java/com/fsck/k9/view/MessageHeader.java b/k9mail/src/main/java/com/fsck/k9/view/MessageHeader.java index 67c5e130d..a536676d7 100644 --- a/k9mail/src/main/java/com/fsck/k9/view/MessageHeader.java +++ b/k9mail/src/main/java/com/fsck/k9/view/MessageHeader.java @@ -153,8 +153,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener { } public void setOnFlagListener(OnClickListener listener) { - if (mFlagged == null) - return; mFlagged.setOnClickListener(listener); } From 2e05127c97e8764fc54cf1dfa627c145f9b1b44f Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 14 Jan 2015 09:56:57 +0100 Subject: [PATCH 020/152] Use a Loader to extract text of a message in a background thread --- .../k9/mailstore/LocalMessageExtractor.java | 6 ++ .../fsck/k9/mailstore/MessageViewInfo.java | 21 +++++ .../k9/ui/message/DecodeMessageLoader.java | 49 ++++++++++ .../ui/messageview/MessageViewFragment.java | 42 ++++++++- .../k9/ui/messageview/SingleMessageView.java | 48 ++++------ .../java/com/fsck/k9/view/AttachmentView.java | 89 +++++++++---------- 6 files changed, 176 insertions(+), 79 deletions(-) create mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java create mode 100644 k9mail/src/main/java/com/fsck/k9/ui/message/DecodeMessageLoader.java diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java index 229a166fe..6a4b61170 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java @@ -467,4 +467,10 @@ public class LocalMessageExtractor { } return new ViewableContainer(text, html, attachments); } + + public static MessageViewInfo decodeMessageForView(Context context, Message message) throws MessagingException { + //TODO: Modify extractTextAndAttachments() to only extract the text type (plain vs. HTML) we currently need. + ViewableContainer viewable = LocalMessageExtractor.extractTextAndAttachments(context, message); + return new MessageViewInfo(viewable.html, viewable.attachments, message); + } } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java new file mode 100644 index 000000000..6e359bbbc --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java @@ -0,0 +1,21 @@ +package com.fsck.k9.mailstore; + + +import java.util.Collections; +import java.util.List; + +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.Part; + + +public class MessageViewInfo { + public final String text; + public final List attachments; + public final Message message; + + public MessageViewInfo(String text, List attachments, Message message) { + this.text = text; + this.attachments = Collections.unmodifiableList(attachments); + this.message = message; + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/ui/message/DecodeMessageLoader.java b/k9mail/src/main/java/com/fsck/k9/ui/message/DecodeMessageLoader.java new file mode 100644 index 000000000..27a0a5e00 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/ui/message/DecodeMessageLoader.java @@ -0,0 +1,49 @@ +package com.fsck.k9.ui.message; + + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.util.Log; + +import com.fsck.k9.K9; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mailstore.LocalMessageExtractor; +import com.fsck.k9.mailstore.MessageViewInfo; + + +public class DecodeMessageLoader extends AsyncTaskLoader { + private final Message message; + private MessageViewInfo messageViewInfo; + + public DecodeMessageLoader(Context context, Message message) { + super(context); + this.message = message; + } + + @Override + protected void onStartLoading() { + if (messageViewInfo != null) { + super.deliverResult(messageViewInfo); + } + + if (takeContentChanged() || messageViewInfo == null) { + forceLoad(); + } + } + + @Override + public void deliverResult(MessageViewInfo messageViewInfo) { + this.messageViewInfo = messageViewInfo; + super.deliverResult(messageViewInfo); + } + + @Override + public MessageViewInfo loadInBackground() { + try { + return LocalMessageExtractor.decodeMessageForView(getContext(), message); + } catch (Exception e) { + Log.e(K9.LOG_TAG, "Error while decoding message", e); + return null; + } + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java index 47740d65f..4ced13aaf 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java @@ -44,6 +44,8 @@ import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.mailstore.LocalMessage; +import com.fsck.k9.mailstore.MessageViewInfo; +import com.fsck.k9.ui.message.DecodeMessageLoader; import com.fsck.k9.ui.message.LocalMessageLoader; import com.fsck.k9.view.AttachmentView; import com.fsck.k9.view.AttachmentView.AttachmentFileDownloadCallback; @@ -65,6 +67,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener, private static final int ACTIVITY_CHOOSE_DIRECTORY = 3; private static final int LOCAL_MESSAGE_LOADER_ID = 1; + private static final int DECODE_MESSAGE_LOADER_ID = 2; public static MessageViewFragment newInstance(MessageReference reference) { @@ -112,6 +115,8 @@ public class MessageViewFragment extends Fragment implements OnClickListener, private Context mContext; private LoaderCallbacks localMessageLoaderCallback = new LocalMessageLoaderCallback(); + private LoaderCallbacks decodeMessageLoaderCallback = new DecodeMessageLoaderCallback(); + private MessageViewInfo messageViewInfo; class MessageViewHandler extends Handler { @@ -301,11 +306,19 @@ public class MessageViewFragment extends Fragment implements OnClickListener, } private void startExtractingTextAndAttachments(LocalMessage message) { - //TODO: extract in background thread + getLoaderManager().initLoader(DECODE_MESSAGE_LOADER_ID, null, decodeMessageLoaderCallback); + } + + private void onDecodeMessageFinished(MessageViewInfo messageContainer) { //TODO: handle decryption and signature verification + this.messageViewInfo = messageContainer; + showMessage(messageContainer); + } + + private void showMessage(MessageViewInfo messageContainer) { try { - mMessageView.setMessage(mAccount, message, mPgpData, mController, mListener); - mMessageView.setShowDownloadButton(message); + mMessageView.setMessage(mAccount, messageContainer, mPgpData, mController, mListener); + mMessageView.setShowDownloadButton(mMessage); } catch (MessagingException e) { Log.e(K9.LOG_TAG, "Error while trying to display message", e); } @@ -656,7 +669,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener, PgpData data = new PgpData(); data.setDecryptedData(decryptedData); data.setSignatureResult(signatureResult); - mMessageView.setMessage(mAccount, (LocalMessage) mMessage, data, mController, mListener); + mMessageView.setMessage(mAccount, messageViewInfo, data, mController, mListener); } catch (MessagingException e) { Log.e(K9.LOG_TAG, "displayMessageBody failed", e); } @@ -809,11 +822,13 @@ public class MessageViewFragment extends Fragment implements OnClickListener, class LocalMessageLoaderCallback implements LoaderCallbacks { @Override public Loader onCreateLoader(int id, Bundle args) { + setProgress(true); return new LocalMessageLoader(mContext, mController, mAccount, mMessageReference); } @Override public void onLoadFinished(Loader loader, LocalMessage message) { + setProgress(false); mMessage = message; if (message == null) { onLoadMessageFromDatabaseFailed(); @@ -827,4 +842,23 @@ public class MessageViewFragment extends Fragment implements OnClickListener, // Do nothing } } + + class DecodeMessageLoaderCallback implements LoaderCallbacks { + @Override + public Loader onCreateLoader(int id, Bundle args) { + setProgress(true); + return new DecodeMessageLoader(mContext, mMessage); + } + + @Override + public void onLoadFinished(Loader loader, MessageViewInfo messageContainer) { + setProgress(false); + onDecodeMessageFinished(messageContainer); + } + + @Override + public void onLoaderReset(Loader loader) { + // Do nothing + } + } } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java index fbe7d01f4..5c8afbb12 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java @@ -52,13 +52,9 @@ import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MimeUtility; -import com.fsck.k9.mailstore.LocalAttachmentBodyPart; -import com.fsck.k9.mailstore.LocalMessage; -import com.fsck.k9.mailstore.LocalMessageExtractor; -import com.fsck.k9.mailstore.ViewableContainer; +import com.fsck.k9.mailstore.MessageViewInfo; import com.fsck.k9.provider.AttachmentProvider.AttachmentProviderColumns; import com.fsck.k9.view.AttachmentView; @@ -502,7 +498,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, return mHeaderContainer.additionalHeadersVisible(); } - public void setMessage(Account account, LocalMessage message, PgpData pgpData, + public void setMessage(Account account, MessageViewInfo messageViewInfo, PgpData pgpData, MessagingController controller, MessagingListener listener) throws MessagingException { resetView(); @@ -515,18 +511,16 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, } if (text == null) { - //FIXME: Run the text extraction in a background thread because it might involve disk I/O - ViewableContainer viewables = LocalMessageExtractor.extractTextAndAttachments(getContext(), message); - text = viewables.html; + text = messageViewInfo.text; } // Save the text so we can reset the WebView when the user clicks the "Show pictures" button mText = text; - mHasAttachments = message.hasAttachments(); + mHasAttachments = !messageViewInfo.attachments.isEmpty(); if (mHasAttachments) { - renderAttachments(message, 0, message, account, controller, listener); + renderAttachments(messageViewInfo, account, controller, listener); } mHiddenAttachments.setVisibility(View.GONE); @@ -558,7 +552,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, // button wasn't already pressed, see if the user's preferences has us // showing them anyway. if (Utility.hasExternalImages(text) && !showPictures()) { - Address[] from = message.getFrom(); + Address[] from = messageViewInfo.message.getFrom(); if ((account.getShowPictures() == Account.ShowPictures.ALWAYS) || ((account.getShowPictures() == Account.ShowPictures.ONLY_FROM_CONTACTS) && // Make sure we have at least one from address @@ -574,7 +568,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, if (text != null) { loadBodyFromText(text); mOpenPgpView.updateLayout(account, pgpData.getDecryptedData(), - pgpData.getSignatureResult(), message); + pgpData.getSignatureResult(), messageViewInfo.message); } else { showStatusMessage(getContext().getString(R.string.webview_empty_message)); } @@ -613,26 +607,20 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, } } - public void renderAttachments(Part part, int depth, Message message, Account account, - MessagingController controller, MessagingListener listener) throws MessagingException { + public void renderAttachments(MessageViewInfo messageContainer, Account account, MessagingController controller, + MessagingListener listener) throws MessagingException { - if (part.getBody() instanceof Multipart) { - Multipart mp = (Multipart) part.getBody(); - for (int i = 0; i < mp.getCount(); i++) { - renderAttachments(mp.getBodyPart(i), depth + 1, message, account, controller, listener); - } - } else if (part instanceof LocalAttachmentBodyPart) { - AttachmentView view = (AttachmentView)mInflater.inflate(R.layout.message_view_attachment, null); + for (Part attachment : messageContainer.attachments) { + AttachmentView view = (AttachmentView) mInflater.inflate(R.layout.message_view_attachment, null); view.setCallback(attachmentCallback); - try { - if (view.populateFromPart(part, message, account, controller, listener)) { - addAttachment(view); - } else { - addHiddenAttachment(view); - } - } catch (Exception e) { - Log.e(K9.LOG_TAG, "Error adding attachment view", e); + boolean isFirstClassAttachment = view.populateFromPart(attachment, messageContainer.message, account, + controller, listener); + + if (isFirstClassAttachment) { + addAttachment(view); + } else { + addHiddenAttachment(view); } } } diff --git a/k9mail/src/main/java/com/fsck/k9/view/AttachmentView.java b/k9mail/src/main/java/com/fsck/k9/view/AttachmentView.java index 7552c23b5..de3d27bf7 100644 --- a/k9mail/src/main/java/com/fsck/k9/view/AttachmentView.java +++ b/k9mail/src/main/java/com/fsck/k9/view/AttachmentView.java @@ -2,10 +2,7 @@ package com.fsck.k9.view; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.util.List; import android.content.ActivityNotFoundException; @@ -14,7 +11,6 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.net.Uri; import android.os.AsyncTask; import android.os.Environment; @@ -43,15 +39,12 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeUtility; -import com.fsck.k9.mailstore.LocalAttachmentBodyPart; -import com.fsck.k9.provider.AttachmentProvider; -import org.apache.commons.io.IOUtils; public class AttachmentView extends FrameLayout implements OnClickListener, OnLongClickListener { private Context context; private Message message; - private LocalAttachmentBodyPart part; + private Part part; private Account account; private MessagingController controller; private MessagingListener listener; @@ -96,10 +89,10 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo * * @return {@code true} for a regular attachment. {@code false} for attachments that should be initially hidden. */ - public boolean populateFromPart(Part inputPart, Message message, Account account, + public boolean populateFromPart(Part part, Message message, Account account, MessagingController controller, MessagingListener listener) throws MessagingException { - part = (LocalAttachmentBodyPart) inputPart; + this.part = part; this.message = message; this.account = account; this.controller = controller; @@ -247,19 +240,21 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo } private void writeAttachmentToStorage(File file) throws IOException { - Uri uri = AttachmentProvider.getAttachmentUri(account, part.getAttachmentId()); - InputStream in = context.getContentResolver().openInputStream(uri); - try { - OutputStream out = new FileOutputStream(file); - try { - IOUtils.copy(in, out); - out.flush(); - } finally { - out.close(); - } - } finally { - in.close(); - } + //FIXME + throw new RuntimeException("temporarily disabled"); +// Uri uri = AttachmentProvider.getAttachmentUri(account, part.getAttachmentId()); +// InputStream in = context.getContentResolver().openInputStream(uri); +// try { +// OutputStream out = new FileOutputStream(file); +// try { +// IOUtils.copy(in, out); +// out.flush(); +// } finally { +// out.close(); +// } +// } finally { +// in.close(); +// } } public void showFile() { @@ -323,14 +318,16 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo } private Intent createViewIntentForAttachmentProviderUri(String mimeType) { - Uri uri = AttachmentProvider.getAttachmentUriForViewing(account, part.getAttachmentId(), mimeType, name); - - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(uri, mimeType); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - addUiIntentFlags(intent); - - return intent; + //FIXME + throw new RuntimeException("temporarily disabled"); +// Uri uri = AttachmentProvider.getAttachmentUriForViewing(account, part.getAttachmentId(), mimeType, name); +// +// Intent intent = new Intent(Intent.ACTION_VIEW); +// intent.setDataAndType(uri, mimeType); +// intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); +// addUiIntentFlags(intent); +// +// return intent; } private Intent createViewIntentForFileUri(String mimeType, Uri uri) { @@ -422,20 +419,22 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo } private Bitmap getPreviewIcon() { - Bitmap icon = null; - try { - InputStream input = context.getContentResolver().openInputStream( - AttachmentProvider.getAttachmentThumbnailUri(account, - part.getAttachmentId(), - 62, - 62)); - icon = BitmapFactory.decodeStream(input); - input.close(); - } catch (Exception e) { - // We don't care what happened, we just return null for the preview icon. - } - - return icon; + //FIXME - temporarily disabled + return null; +// Bitmap icon = null; +// try { +// InputStream input = context.getContentResolver().openInputStream( +// AttachmentProvider.getAttachmentThumbnailUri(account, +// part.getAttachmentId(), +// 62, +// 62)); +// icon = BitmapFactory.decodeStream(input); +// input.close(); +// } catch (Exception e) { +// // We don't care what happened, we just return null for the preview icon. +// } +// +// return icon; } protected void onPostExecute(Bitmap previewIcon) { From 087238f5077fc132f1a19f5278a4e3f5810fd607 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 15 Jan 2015 08:25:43 +0100 Subject: [PATCH 021/152] Move AttachmentView to 'messageview' package --- .../com/fsck/k9/{view => ui/messageview}/AttachmentView.java | 2 +- .../java/com/fsck/k9/ui/messageview/MessageViewFragment.java | 3 +-- .../java/com/fsck/k9/ui/messageview/SingleMessageView.java | 3 +-- k9mail/src/main/res/layout/message_view_attachment.xml | 4 ++-- 4 files changed, 5 insertions(+), 7 deletions(-) rename k9mail/src/main/java/com/fsck/k9/{view => ui/messageview}/AttachmentView.java (99%) diff --git a/k9mail/src/main/java/com/fsck/k9/view/AttachmentView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java similarity index 99% rename from k9mail/src/main/java/com/fsck/k9/view/AttachmentView.java rename to k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java index de3d27bf7..250c3ca13 100644 --- a/k9mail/src/main/java/com/fsck/k9/view/AttachmentView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java @@ -1,4 +1,4 @@ -package com.fsck.k9.view; +package com.fsck.k9.ui.messageview; import java.io.File; diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java index 4ced13aaf..0722930d9 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java @@ -47,8 +47,7 @@ import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.MessageViewInfo; import com.fsck.k9.ui.message.DecodeMessageLoader; import com.fsck.k9.ui.message.LocalMessageLoader; -import com.fsck.k9.view.AttachmentView; -import com.fsck.k9.view.AttachmentView.AttachmentFileDownloadCallback; +import com.fsck.k9.ui.messageview.AttachmentView.AttachmentFileDownloadCallback; import com.fsck.k9.view.MessageHeader; import org.openintents.openpgp.OpenPgpSignatureResult; diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java index 5c8afbb12..586da1a9a 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java @@ -57,8 +57,7 @@ import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.MessageViewInfo; import com.fsck.k9.provider.AttachmentProvider.AttachmentProviderColumns; -import com.fsck.k9.view.AttachmentView; -import com.fsck.k9.view.AttachmentView.AttachmentFileDownloadCallback; +import com.fsck.k9.ui.messageview.AttachmentView.AttachmentFileDownloadCallback; import com.fsck.k9.view.MessageHeader; import com.fsck.k9.view.MessageHeader.OnLayoutChangedListener; import com.fsck.k9.view.MessageWebView; diff --git a/k9mail/src/main/res/layout/message_view_attachment.xml b/k9mail/src/main/res/layout/message_view_attachment.xml index 1e338b21b..4be7cc56a 100644 --- a/k9mail/src/main/res/layout/message_view_attachment.xml +++ b/k9mail/src/main/res/layout/message_view_attachment.xml @@ -1,5 +1,5 @@ - - + From 8fce9e36542739ad834890ad6ccbaa24b8a81fc5 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 15 Jan 2015 11:19:32 +0100 Subject: [PATCH 022/152] Move functionality from AttachmentView to AttachmentController --- .../fsck/k9/mailstore/AttachmentViewInfo.java | 26 ++ .../k9/mailstore/LocalMessageExtractor.java | 86 ++-- .../fsck/k9/mailstore/MessageViewInfo.java | 5 +- .../ui/messageview/AttachmentController.java | 259 ++++++++++++ .../k9/ui/messageview/AttachmentView.java | 369 ++---------------- .../messageview/AttachmentViewCallback.java | 11 + .../ui/messageview/MessageViewFragment.java | 228 +++-------- .../k9/ui/messageview/SingleMessageView.java | 61 ++- 8 files changed, 458 insertions(+), 587 deletions(-) create mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java create mode 100644 k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java create mode 100644 k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentViewCallback.java diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java b/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java new file mode 100644 index 000000000..8b0bb3050 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java @@ -0,0 +1,26 @@ +package com.fsck.k9.mailstore; + + +import android.net.Uri; + +import com.fsck.k9.mail.Part; + + +public class AttachmentViewInfo { + public final String mimeType; + public final String displayName; + public final long size; + public final Uri uri; + public final boolean firstClassAttachment; + public final Part part; + + public AttachmentViewInfo(String mimeType, String displayName, long size, Uri uri, boolean firstClassAttachment, + Part part) { + this.mimeType = mimeType; + this.displayName = displayName; + this.size = size; + this.uri = uri; + this.firstClassAttachment = firstClassAttachment; + this.part = part; + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java index 6a4b61170..fc867b61a 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java @@ -1,6 +1,7 @@ package com.fsck.k9.mailstore; import android.content.Context; +import android.net.Uri; import com.fsck.k9.R; import com.fsck.k9.mail.Address; @@ -11,7 +12,9 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.mail.internet.MessageExtractor; +import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.Viewable; import java.util.ArrayList; @@ -118,35 +121,6 @@ public class LocalMessageExtractor { } } - public static ViewableContainer extractPartsFromDraft(Message message) - throws MessagingException { - - Body body = message.getBody(); - if (message.isMimeType("multipart/mixed") && body instanceof MimeMultipart) { - MimeMultipart multipart = (MimeMultipart) body; - - ViewableContainer container; - int count = multipart.getCount(); - if (count >= 1) { - // The first part is either a text/plain or a multipart/alternative - BodyPart firstPart = multipart.getBodyPart(0); - container = extractTextual(firstPart); - - // The rest should be attachments - for (int i = 1; i < count; i++) { - BodyPart bodyPart = multipart.getBodyPart(i); - container.attachments.add(bodyPart); - } - } else { - container = new ViewableContainer("", "", new ArrayList()); - } - - return container; - } - - return extractTextual(message); - } - /** * Use the contents of a {@link com.fsck.k9.mail.internet.Viewable} to create the HTML to be displayed. * @@ -471,6 +445,58 @@ public class LocalMessageExtractor { public static MessageViewInfo decodeMessageForView(Context context, Message message) throws MessagingException { //TODO: Modify extractTextAndAttachments() to only extract the text type (plain vs. HTML) we currently need. ViewableContainer viewable = LocalMessageExtractor.extractTextAndAttachments(context, message); - return new MessageViewInfo(viewable.html, viewable.attachments, message); + List attachments = extractAttachmentInfos(viewable.attachments); + return new MessageViewInfo(viewable.html, attachments, message); + } + + private static List extractAttachmentInfos(List attachmentParts) + throws MessagingException { + + List attachments = new ArrayList(); + for (Part part : attachmentParts) { + attachments.add(extractAttachmentInfo(part)); + } + + return attachments; + } + + private static AttachmentViewInfo extractAttachmentInfo(Part part) throws MessagingException { + boolean firstClassAttachment = true; + + String mimeType = part.getMimeType(); + String contentTypeHeader = MimeUtility.unfoldAndDecode(part.getContentType()); + String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition()); + + String name = MimeUtility.getHeaderParameter(contentDisposition, "filename"); + if (name == null) { + name = MimeUtility.getHeaderParameter(contentTypeHeader, "name"); + } + + if (name == null) { + firstClassAttachment = false; + String extension = MimeUtility.getExtensionByMimeType(mimeType); + name = "noname" + ((extension != null) ? "." + extension : ""); + } + + // Inline parts with a content-id are almost certainly components of an HTML message + // not attachments. Only show them if the user pressed the button to show more + // attachments. + if (contentDisposition != null && + MimeUtility.getHeaderParameter(contentDisposition, null).matches("^(?i:inline)") && + part.getHeader(MimeHeader.HEADER_CONTENT_ID) != null) { + firstClassAttachment = false; + } + + long size = 0; + String sizeParam = MimeUtility.getHeaderParameter(contentDisposition, "size"); + if (sizeParam != null) { + try { + size = Integer.parseInt(sizeParam); + } catch (NumberFormatException e) { /* ignore */ } + } + + Uri uri = Uri.parse("dummy://this.needs.fixing"); //FIXME + + return new AttachmentViewInfo(mimeType, name, size, uri, firstClassAttachment, part); } } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java index 6e359bbbc..98f8fc2b2 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java @@ -5,15 +5,14 @@ import java.util.Collections; import java.util.List; import com.fsck.k9.mail.Message; -import com.fsck.k9.mail.Part; public class MessageViewInfo { public final String text; - public final List attachments; + public final List attachments; public final Message message; - public MessageViewInfo(String text, List attachments, Message message) { + public MessageViewInfo(String text, List attachments, Message message) { this.text = text; this.attachments = Collections.unmodifiableList(attachments); this.message = message; diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java new file mode 100644 index 000000000..fe8cd0f8a --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java @@ -0,0 +1,259 @@ +package com.fsck.k9.ui.messageview; + + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Environment; +import android.util.Log; +import android.widget.Toast; + +import com.fsck.k9.K9; +import com.fsck.k9.R; +import com.fsck.k9.cache.TemporaryAttachmentStore; +import com.fsck.k9.helper.FileHelper; +import com.fsck.k9.helper.MediaScannerNotifier; +import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mailstore.AttachmentViewInfo; +import org.apache.commons.io.IOUtils; + + +public class AttachmentController { + private final Context context; + private final SingleMessageView messageView; + private final AttachmentViewInfo attachment; + + AttachmentController(SingleMessageView messageView, AttachmentViewInfo attachment) { + this.context = messageView.getContext(); + this.messageView = messageView; + this.attachment = attachment; + } + + public void viewAttachment() { + new ViewAttachmentAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + public void saveAttachment() { + saveAttachmentTo(K9.getAttachmentDefaultPath()); + } + + public void saveAttachmentTo(String directory) { + saveAttachmentTo(new File(directory)); + } + + private void saveAttachmentTo(File directory) { + boolean isExternalStorageMounted = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); + if (!isExternalStorageMounted) { + String message = context.getString(R.string.message_view_status_attachment_not_saved); + displayMessageToUser(message); + return; + } + + //FIXME: write file in background thread + try { + File file = saveAttachmentWithUniqueFileName(directory); + + displayAttachmentSavedMessage(file.toString()); + + MediaScannerNotifier.notify(context, file); + } catch (IOException ioe) { + if (K9.DEBUG) { + Log.e(K9.LOG_TAG, "Error saving attachment", ioe); + } + displayAttachmentNotSavedMessage(); + } + } + + private File saveAttachmentWithUniqueFileName(File directory) throws IOException { + String filename = FileHelper.sanitizeFilename(attachment.displayName); + File file = FileHelper.createUniqueFile(directory, filename); + + writeAttachmentToStorage(file); + + return file; + } + + private void writeAttachmentToStorage(File file) throws IOException { + InputStream in = context.getContentResolver().openInputStream(attachment.uri); + try { + OutputStream out = new FileOutputStream(file); + try { + IOUtils.copy(in, out); + out.flush(); + } finally { + out.close(); + } + } finally { + in.close(); + } + } + + private Intent getBestViewIntentAndSaveFileIfNecessary() { + String displayName = attachment.displayName; + String inferredMimeType = MimeUtility.getMimeTypeByExtension(displayName); + + IntentAndResolvedActivitiesCount resolvedIntentInfo; + String mimeType = attachment.mimeType; + if (MimeUtility.isDefaultMimeType(mimeType)) { + resolvedIntentInfo = getBestViewIntentForMimeType(inferredMimeType); + } else { + resolvedIntentInfo = getBestViewIntentForMimeType(mimeType); + if (!resolvedIntentInfo.hasResolvedActivities() && !inferredMimeType.equals(mimeType)) { + resolvedIntentInfo = getBestViewIntentForMimeType(inferredMimeType); + } + } + + if (!resolvedIntentInfo.hasResolvedActivities()) { + resolvedIntentInfo = getBestViewIntentForMimeType(MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE); + } + + Intent viewIntent; + if (resolvedIntentInfo.hasResolvedActivities() && resolvedIntentInfo.containsFileUri()) { + try { + File tempFile = TemporaryAttachmentStore.getFileForWriting(context, displayName); + writeAttachmentToStorage(tempFile); + viewIntent = createViewIntentForFileUri(resolvedIntentInfo.getMimeType(), Uri.fromFile(tempFile)); + } catch (IOException e) { + if (K9.DEBUG) { + Log.e(K9.LOG_TAG, "Error while saving attachment to use file:// URI with ACTION_VIEW Intent", e); + } + viewIntent = createViewIntentForAttachmentProviderUri(MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE); + } + } else { + viewIntent = resolvedIntentInfo.getIntent(); + } + + return viewIntent; + } + + private IntentAndResolvedActivitiesCount getBestViewIntentForMimeType(String mimeType) { + Intent contentUriIntent = createViewIntentForAttachmentProviderUri(mimeType); + int contentUriActivitiesCount = getResolvedIntentActivitiesCount(contentUriIntent); + + if (contentUriActivitiesCount > 0) { + return new IntentAndResolvedActivitiesCount(contentUriIntent, contentUriActivitiesCount); + } + + File tempFile = TemporaryAttachmentStore.getFile(context, attachment.displayName); + Uri tempFileUri = Uri.fromFile(tempFile); + Intent fileUriIntent = createViewIntentForFileUri(mimeType, tempFileUri); + int fileUriActivitiesCount = getResolvedIntentActivitiesCount(fileUriIntent); + + if (fileUriActivitiesCount > 0) { + return new IntentAndResolvedActivitiesCount(fileUriIntent, fileUriActivitiesCount); + } + + return new IntentAndResolvedActivitiesCount(contentUriIntent, contentUriActivitiesCount); + } + + private Intent createViewIntentForAttachmentProviderUri(String mimeType) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(attachment.uri, mimeType); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + addUiIntentFlags(intent); + + return intent; + } + + private Intent createViewIntentForFileUri(String mimeType, Uri uri) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(uri, mimeType); + addUiIntentFlags(intent); + + return intent; + } + + private void addUiIntentFlags(Intent intent) { + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + } + + private int getResolvedIntentActivitiesCount(Intent intent) { + PackageManager packageManager = context.getPackageManager(); + + List resolveInfos = + packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + + return resolveInfos.size(); + } + + private void displayAttachmentSavedMessage(final String filename) { + String message = context.getString(R.string.message_view_status_attachment_saved, filename); + displayMessageToUser(message); + } + + private void displayAttachmentNotSavedMessage() { + String message = context.getString(R.string.message_view_status_attachment_not_saved); + displayMessageToUser(message); + } + + private void displayMessageToUser(String message) { + Toast.makeText(context, message, Toast.LENGTH_LONG).show(); + } + + private static class IntentAndResolvedActivitiesCount { + private Intent intent; + private int activitiesCount; + + IntentAndResolvedActivitiesCount(Intent intent, int activitiesCount) { + this.intent = intent; + this.activitiesCount = activitiesCount; + } + + public Intent getIntent() { + return intent; + } + + public boolean hasResolvedActivities() { + return activitiesCount > 0; + } + + public String getMimeType() { + return intent.getType(); + } + + public boolean containsFileUri() { + return "file".equals(intent.getData().getScheme()); + } + } + + private class ViewAttachmentAsyncTask extends AsyncTask { + + @Override + protected void onPreExecute() { + messageView.disableAttachmentViewButton(attachment); + } + + @Override + protected Intent doInBackground(Void... params) { + return getBestViewIntentAndSaveFileIfNecessary(); + } + + @Override + protected void onPostExecute(Intent intent) { + viewAttachment(intent); + messageView.enableAttachmentViewButton(attachment); + } + + private void viewAttachment(Intent intent) { + try { + context.startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.e(K9.LOG_TAG, "Could not display attachment of type " + attachment.mimeType, e); + + String message = context.getString(R.string.message_view_no_viewer, attachment.mimeType); + displayMessageToUser(message); + } + } + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java index 250c3ca13..ac2acb59b 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java @@ -1,21 +1,10 @@ package com.fsck.k9.ui.messageview; -import java.io.File; -import java.io.IOException; -import java.util.List; - -import android.content.ActivityNotFoundException; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.graphics.Bitmap; -import android.net.Uri; import android.os.AsyncTask; -import android.os.Environment; import android.util.AttributeSet; -import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; @@ -23,124 +12,50 @@ import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; -import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.R; -import com.fsck.k9.cache.TemporaryAttachmentStore; -import com.fsck.k9.controller.MessagingController; -import com.fsck.k9.controller.MessagingListener; -import com.fsck.k9.helper.FileHelper; -import com.fsck.k9.helper.MediaScannerNotifier; import com.fsck.k9.helper.SizeFormatter; -import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.Part; -import com.fsck.k9.mail.internet.MimeHeader; -import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mailstore.AttachmentViewInfo; public class AttachmentView extends FrameLayout implements OnClickListener, OnLongClickListener { - private Context context; - private Message message; - private Part part; - private Account account; - private MessagingController controller; - private MessagingListener listener; - private AttachmentFileDownloadCallback callback; + private AttachmentViewInfo attachment; + private AttachmentViewCallback callback; private Button viewButton; private Button downloadButton; - private String name; - private String contentType; - private long size; - public AttachmentView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - this.context = context; } public AttachmentView(Context context, AttributeSet attrs) { super(context, attrs); - this.context = context; } public AttachmentView(Context context) { super(context); - this.context = context; } - public void setButtonsEnabled(boolean enabled) { - viewButton.setEnabled(enabled); - downloadButton.setEnabled(enabled); + public AttachmentViewInfo getAttachment() { + return attachment; } - /** - * Populates this view with information about the attachment. - *

- * This method also decides which attachments are displayed when the "show attachments" button - * is pressed, and which attachments are only displayed after the "show more attachments" - * button was pressed.
- * Inline attachments with content ID and unnamed attachments fall into the second category. - *

- * - * @return {@code true} for a regular attachment. {@code false} for attachments that should be initially hidden. - */ - public boolean populateFromPart(Part part, Message message, Account account, - MessagingController controller, MessagingListener listener) throws MessagingException { + public void enableViewButton() { + viewButton.setEnabled(true); + } - this.part = part; - this.message = message; - this.account = account; - this.controller = controller; - this.listener = listener; + public void disableViewButton() { + viewButton.setEnabled(false); + } - boolean firstClassAttachment = extractAttachmentInformation(part); + public void setAttachment(AttachmentViewInfo attachment) throws MessagingException { + this.attachment = attachment; displayAttachmentInformation(); - - return firstClassAttachment; - } - - //TODO: extract this code to a helper class - private boolean extractAttachmentInformation(Part part) throws MessagingException { - boolean firstClassAttachment = true; - - contentType = part.getMimeType(); - String contentTypeHeader = MimeUtility.unfoldAndDecode(part.getContentType()); - String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition()); - - name = MimeUtility.getHeaderParameter(contentTypeHeader, "name"); - if (name == null) { - name = MimeUtility.getHeaderParameter(contentDisposition, "filename"); - } - - if (name == null) { - firstClassAttachment = false; - String extension = MimeUtility.getExtensionByMimeType(contentType); - name = "noname" + ((extension != null) ? "." + extension : ""); - } - - // Inline parts with a content-id are almost certainly components of an HTML message - // not attachments. Only show them if the user pressed the button to show more - // attachments. - if (contentDisposition != null && - MimeUtility.getHeaderParameter(contentDisposition, null).matches("^(?i:inline)") - && part.getHeader(MimeHeader.HEADER_CONTENT_ID) != null) { - firstClassAttachment = false; - } - - String sizeParam = MimeUtility.getHeaderParameter(contentDisposition, "size"); - if (sizeParam != null) { - try { - size = Integer.parseInt(sizeParam); - } catch (NumberFormatException e) { /* ignore */ } - } - - return firstClassAttachment; } private void displayAttachmentInformation() { @@ -149,7 +64,7 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo viewButton = (Button) findViewById(R.id.view); downloadButton = (Button) findViewById(R.id.download); - if (size > K9.MAX_ATTACHMENT_DOWNLOAD_SIZE) { + if (attachment.size > K9.MAX_ATTACHMENT_DOWNLOAD_SIZE) { viewButton.setVisibility(View.GONE); downloadButton.setVisibility(View.GONE); } @@ -158,8 +73,8 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo downloadButton.setOnClickListener(this); downloadButton.setOnLongClickListener(this); - attachmentName.setText(name); - attachmentInfo.setText(SizeFormatter.formatSize(context, size)); + attachmentName.setText(attachment.displayName); + attachmentInfo.setText(SizeFormatter.formatSize(getContext(), attachment.size)); ImageView thumbnail = (ImageView) findViewById(R.id.attachment_icon); new LoadAndDisplayThumbnailAsyncTask(thumbnail).execute(); @@ -169,11 +84,11 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo public void onClick(View view) { switch (view.getId()) { case R.id.view: { - onViewButtonClicked(); + onViewButtonClick(); break; } case R.id.download: { - onSaveButtonClicked(); + onSaveButtonClick(); break; } } @@ -182,231 +97,29 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo @Override public boolean onLongClick(View view) { if (view.getId() == R.id.download) { - callback.pickDirectoryToSaveAttachmentTo(this); + onSaveButtonLongClick(); return true; } return false; } - private void onViewButtonClicked() { - if (message != null) { - controller.loadAttachment(account, message, part, new Object[] {false, this}, listener); - } + private void onViewButtonClick() { + callback.onViewAttachment(attachment); } - private void onSaveButtonClicked() { - boolean isExternalStorageMounted = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); - if (!isExternalStorageMounted) { - String message = context.getString(R.string.message_view_status_attachment_not_saved); - displayMessageToUser(message); - return; - } - - if (message != null) { - controller.loadAttachment(account, message, part, new Object[] {true, this}, listener); - } + private void onSaveButtonClick() { + callback.onSaveAttachment(attachment); } - public void writeFile() { - writeFile(new File(K9.getAttachmentDefaultPath())); + private void onSaveButtonLongClick() { + callback.onSaveAttachmentToUserProvidedDirectory(attachment); } - /** - * Saves the attachment as file in the given directory - */ - public void writeFile(File directory) { - try { - File file = saveAttachmentWithUniqueFileName(directory); - - displayAttachmentSavedMessage(file.toString()); - - MediaScannerNotifier.notify(context, file); - } catch (IOException ioe) { - if (K9.DEBUG) { - Log.e(K9.LOG_TAG, "Error saving attachment", ioe); - } - displayAttachmentNotSavedMessage(); - } - } - - private File saveAttachmentWithUniqueFileName(File directory) throws IOException { - String filename = FileHelper.sanitizeFilename(name); - File file = FileHelper.createUniqueFile(directory, filename); - - writeAttachmentToStorage(file); - - return file; - } - - private void writeAttachmentToStorage(File file) throws IOException { - //FIXME - throw new RuntimeException("temporarily disabled"); -// Uri uri = AttachmentProvider.getAttachmentUri(account, part.getAttachmentId()); -// InputStream in = context.getContentResolver().openInputStream(uri); -// try { -// OutputStream out = new FileOutputStream(file); -// try { -// IOUtils.copy(in, out); -// out.flush(); -// } finally { -// out.close(); -// } -// } finally { -// in.close(); -// } - } - - public void showFile() { - new ViewAttachmentAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - private Intent getBestViewIntentAndSaveFileIfNecessary() { - String inferredMimeType = MimeUtility.getMimeTypeByExtension(name); - - IntentAndResolvedActivitiesCount resolvedIntentInfo; - if (MimeUtility.isDefaultMimeType(contentType)) { - resolvedIntentInfo = getBestViewIntentForMimeType(inferredMimeType); - } else { - resolvedIntentInfo = getBestViewIntentForMimeType(contentType); - if (!resolvedIntentInfo.hasResolvedActivities() && !inferredMimeType.equals(contentType)) { - resolvedIntentInfo = getBestViewIntentForMimeType(inferredMimeType); - } - } - - if (!resolvedIntentInfo.hasResolvedActivities()) { - resolvedIntentInfo = getBestViewIntentForMimeType(MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE); - } - - Intent viewIntent; - if (resolvedIntentInfo.hasResolvedActivities() && resolvedIntentInfo.containsFileUri()) { - try { - File tempFile = TemporaryAttachmentStore.getFileForWriting(context, name); - writeAttachmentToStorage(tempFile); - viewIntent = createViewIntentForFileUri(resolvedIntentInfo.getMimeType(), Uri.fromFile(tempFile)); - } catch (IOException e) { - if (K9.DEBUG) { - Log.e(K9.LOG_TAG, "Error while saving attachment to use file:// URI with ACTION_VIEW Intent", e); - } - viewIntent = createViewIntentForAttachmentProviderUri(MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE); - } - } else { - viewIntent = resolvedIntentInfo.getIntent(); - } - - return viewIntent; - } - - private IntentAndResolvedActivitiesCount getBestViewIntentForMimeType(String mimeType) { - Intent contentUriIntent = createViewIntentForAttachmentProviderUri(mimeType); - int contentUriActivitiesCount = getResolvedIntentActivitiesCount(contentUriIntent); - - if (contentUriActivitiesCount > 0) { - return new IntentAndResolvedActivitiesCount(contentUriIntent, contentUriActivitiesCount); - } - - File tempFile = TemporaryAttachmentStore.getFile(context, name); - Uri tempFileUri = Uri.fromFile(tempFile); - Intent fileUriIntent = createViewIntentForFileUri(mimeType, tempFileUri); - int fileUriActivitiesCount = getResolvedIntentActivitiesCount(fileUriIntent); - - if (fileUriActivitiesCount > 0) { - return new IntentAndResolvedActivitiesCount(fileUriIntent, fileUriActivitiesCount); - } - - return new IntentAndResolvedActivitiesCount(contentUriIntent, contentUriActivitiesCount); - } - - private Intent createViewIntentForAttachmentProviderUri(String mimeType) { - //FIXME - throw new RuntimeException("temporarily disabled"); -// Uri uri = AttachmentProvider.getAttachmentUriForViewing(account, part.getAttachmentId(), mimeType, name); -// -// Intent intent = new Intent(Intent.ACTION_VIEW); -// intent.setDataAndType(uri, mimeType); -// intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); -// addUiIntentFlags(intent); -// -// return intent; - } - - private Intent createViewIntentForFileUri(String mimeType, Uri uri) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(uri, mimeType); - addUiIntentFlags(intent); - - return intent; - } - - private void addUiIntentFlags(Intent intent) { - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); - } - - private int getResolvedIntentActivitiesCount(Intent intent) { - PackageManager packageManager = context.getPackageManager(); - - List resolveInfos = - packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - - return resolveInfos.size(); - } - - private void displayAttachmentSavedMessage(final String filename) { - String message = context.getString(R.string.message_view_status_attachment_saved, filename); - displayMessageToUser(message); - } - - private void displayAttachmentNotSavedMessage() { - String message = context.getString(R.string.message_view_status_attachment_not_saved); - displayMessageToUser(message); - } - - private void displayMessageToUser(String message) { - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - } - - public void setCallback(AttachmentFileDownloadCallback callback) { + public void setCallback(AttachmentViewCallback callback) { this.callback = callback; } - - public interface AttachmentFileDownloadCallback { - /** - * This method is called to ask the user to pick a directory to save the attachment to. - *

- * After the user has selected a directory, the implementation of this interface has to call - * {@link #writeFile(File)} on the object supplied as argument in order for the attachment to be saved. - */ - public void pickDirectoryToSaveAttachmentTo(AttachmentView caller); - } - - - private static class IntentAndResolvedActivitiesCount { - private Intent intent; - private int activitiesCount; - - IntentAndResolvedActivitiesCount(Intent intent, int activitiesCount) { - this.intent = intent; - this.activitiesCount = activitiesCount; - } - - public Intent getIntent() { - return intent; - } - - public boolean hasResolvedActivities() { - return activitiesCount > 0; - } - - public String getMimeType() { - return intent.getType(); - } - - public boolean containsFileUri() { - return "file".equals(intent.getData().getScheme()); - } - } - private class LoadAndDisplayThumbnailAsyncTask extends AsyncTask { private final ImageView thumbnail; @@ -445,34 +158,4 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo } } } - - private class ViewAttachmentAsyncTask extends AsyncTask { - - @Override - protected void onPreExecute() { - viewButton.setEnabled(false); - } - - @Override - protected Intent doInBackground(Void... params) { - return getBestViewIntentAndSaveFileIfNecessary(); - } - - @Override - protected void onPostExecute(Intent intent) { - viewAttachment(intent); - viewButton.setEnabled(true); - } - - private void viewAttachment(Intent intent) { - try { - context.startActivity(intent); - } catch (ActivityNotFoundException e) { - Log.e(K9.LOG_TAG, "Could not display attachment of type " + contentType, e); - - String message = context.getString(R.string.message_view_no_viewer, contentType); - displayMessageToUser(message); - } - } - } } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentViewCallback.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentViewCallback.java new file mode 100644 index 000000000..3575bee69 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentViewCallback.java @@ -0,0 +1,11 @@ +package com.fsck.k9.ui.messageview; + + +import com.fsck.k9.mailstore.AttachmentViewInfo; + + +interface AttachmentViewCallback { + void onViewAttachment(AttachmentViewInfo attachment); + void onSaveAttachment(AttachmentViewInfo attachment); + void onSaveAttachmentToUserProvidedDirectory(AttachmentViewInfo attachment); +} diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java index 0722930d9..719708e1a 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java @@ -1,20 +1,19 @@ package com.fsck.k9.ui.messageview; -import java.io.File; + import java.util.Collections; import java.util.Locale; import android.app.Activity; +import android.app.DialogFragment; import android.app.Fragment; +import android.app.FragmentManager; import android.app.LoaderManager.LoaderCallbacks; import android.content.Context; import android.content.Intent; import android.content.Loader; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; -import android.app.DialogFragment; -import android.app.FragmentManager; import android.text.TextUtils; import android.util.Log; import android.view.ContextThemeWrapper; @@ -32,7 +31,6 @@ import com.fsck.k9.R; import com.fsck.k9.activity.ChooseFolder; import com.fsck.k9.activity.MessageReference; import com.fsck.k9.controller.MessagingController; -import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.crypto.PgpData; import com.fsck.k9.fragment.ConfirmationDialogFragment; import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener; @@ -40,21 +38,18 @@ import com.fsck.k9.fragment.ProgressDialogFragment; import com.fsck.k9.helper.FileBrowserHelper; import com.fsck.k9.helper.FileBrowserHelper.FileBrowserFailOverCallback; import com.fsck.k9.mail.Flag; -import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.Part; +import com.fsck.k9.mailstore.AttachmentViewInfo; import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.MessageViewInfo; import com.fsck.k9.ui.message.DecodeMessageLoader; import com.fsck.k9.ui.message.LocalMessageLoader; -import com.fsck.k9.ui.messageview.AttachmentView.AttachmentFileDownloadCallback; import com.fsck.k9.view.MessageHeader; - import org.openintents.openpgp.OpenPgpSignatureResult; -public class MessageViewFragment extends Fragment implements OnClickListener, - ConfirmationDialogFragmentListener { +public class MessageViewFragment extends Fragment implements OnClickListener, ConfirmationDialogFragmentListener, + AttachmentViewCallback { private static final String ARG_REFERENCE = "reference"; @@ -86,16 +81,8 @@ public class MessageViewFragment extends Fragment implements OnClickListener, private MessageReference mMessageReference; private LocalMessage mMessage; private MessagingController mController; - private Listener mListener = new Listener(); - private MessageViewHandler mHandler = new MessageViewHandler(); private LayoutInflater mLayoutInflater; - /** this variable is used to save the calling AttachmentView - * until the onActivityResult is called. - * => with this reference we can identity the caller - */ - private AttachmentView attachmentTmpStore; - /** * Used to temporarily store the destination folder for refile operations if a confirmation * dialog is shown. @@ -116,49 +103,9 @@ public class MessageViewFragment extends Fragment implements OnClickListener, private LoaderCallbacks localMessageLoaderCallback = new LocalMessageLoaderCallback(); private LoaderCallbacks decodeMessageLoaderCallback = new DecodeMessageLoaderCallback(); private MessageViewInfo messageViewInfo; + private AttachmentViewInfo currentAttachmentViewInfo; - class MessageViewHandler extends Handler { - - public void progress(final boolean progress) { - post(new Runnable() { - @Override - public void run() { - setProgress(progress); - } - }); - } - - /* A helper for a set of "show a toast" methods */ - private void showToast(final String message, final int toastLength) { - post(new Runnable() { - @Override - public void run() { - Toast.makeText(getActivity(), message, toastLength).show(); - } - }); - } - - public void networkError() { - // FIXME: This is a hack. Fix the Handler madness! - Context context = getActivity(); - if (context == null) { - return; - } - - showToast(context.getString(R.string.status_network_error), Toast.LENGTH_LONG); - } - - public void fetchingAttachment() { - Context context = getActivity(); - if (context == null) { - return; - } - - showToast(context.getString(R.string.message_view_fetching_attachment_toast), Toast.LENGTH_SHORT); - } - } - @Override public void onAttach(Activity activity) { super.onAttach(activity); @@ -195,33 +142,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener, mMessageView = (SingleMessageView) view.findViewById(R.id.message_view); - //set a callback for the attachment view. With this callback the attachmentview - //request the start of a filebrowser activity. - mMessageView.setAttachmentCallback(new AttachmentFileDownloadCallback() { - - @Override - public void pickDirectoryToSaveAttachmentTo(final AttachmentView caller) { - FileBrowserHelper.getInstance() - .showFileBrowserActivity(MessageViewFragment.this, - null, - ACTIVITY_CHOOSE_DIRECTORY, - callback); - attachmentTmpStore = caller; - } - - FileBrowserFailOverCallback callback = new FileBrowserFailOverCallback() { - - @Override - public void onPathEntered(String path) { - attachmentTmpStore.writeFile(new File(path)); - } - - @Override - public void onCancel() { - // canceled, do nothing - } - }; - }); + mMessageView.setAttachmentCallback(this); mMessageView.initialize(this, new OnClickListener() { @Override @@ -246,7 +167,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener, messageReference = (MessageReference) savedInstanceState.get(STATE_MESSAGE_REFERENCE); } else { Bundle args = getArguments(); - messageReference = (MessageReference) args.getParcelable(ARG_REFERENCE); + messageReference = args.getParcelable(ARG_REFERENCE); } displayMessage(messageReference, (mPgpData == null)); @@ -316,7 +237,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener, private void showMessage(MessageViewInfo messageContainer) { try { - mMessageView.setMessage(mAccount, messageContainer, mPgpData, mController, mListener); + mMessageView.setMessage(mAccount, messageContainer, mPgpData); mMessageView.setShowDownloadButton(mMessage); } catch (MessagingException e) { Log.e(K9.LOG_TAG, "Error while trying to display message", e); @@ -474,13 +395,13 @@ public class MessageViewFragment extends Fragment implements OnClickListener, switch (requestCode) { case ACTIVITY_CHOOSE_DIRECTORY: { - if (resultCode == Activity.RESULT_OK && data != null) { + if (data != null) { // obtain the filename Uri fileUri = data.getData(); if (fileUri != null) { String filePath = fileUri.getPath(); if (filePath != null) { - attachmentTmpStore.writeFile(new File(filePath)); + getAttachmentController(currentAttachmentViewInfo).saveAttachmentTo(filePath); } } } @@ -535,7 +456,8 @@ public class MessageViewFragment extends Fragment implements OnClickListener, return; } mMessageView.downloadRemainderButton().setEnabled(false); - mController.loadMessageForViewRemote(mAccount, mMessageReference.folderName, mMessageReference.uid, mListener); + //FIXME +// mController.loadMessageForViewRemote(mAccount, mMessageReference.folderName, mMessageReference.uid, mListener); } @Override @@ -576,89 +498,6 @@ public class MessageViewFragment extends Fragment implements OnClickListener, destFolderName, null); } - class Listener extends MessagingListener { - @Override - public void loadMessageForViewHeadersAvailable(final Account account, String folder, String uid, - final Message message) { - throw new IllegalStateException(); - } - - @Override - public void loadMessageForViewBodyAvailable(final Account account, String folder, - String uid, final Message message) { - throw new IllegalStateException(); - } - - @Override - public void loadMessageForViewFailed(Account account, String folder, String uid, final Throwable t) { - throw new IllegalStateException(); - } - - @Override - public void loadMessageForViewFinished(Account account, String folder, String uid, final Message message) { - throw new IllegalStateException(); - } - - @Override - public void loadMessageForViewStarted(Account account, String folder, String uid) { - throw new IllegalStateException(); - } - - @Override - public void loadAttachmentStarted(Account account, Message message, Part part, Object tag, final boolean requiresDownload) { - if (mMessage != message) { - return; - } - mHandler.post(new Runnable() { - @Override - public void run() { - mMessageView.setAttachmentsEnabled(false); - showDialog(R.id.dialog_attachment_progress); - if (requiresDownload) { - mHandler.fetchingAttachment(); - } - } - }); - } - - @Override - public void loadAttachmentFinished(Account account, Message message, Part part, final Object tag) { - if (mMessage != message) { - return; - } - mHandler.post(new Runnable() { - @Override - public void run() { - mMessageView.setAttachmentsEnabled(true); - removeDialog(R.id.dialog_attachment_progress); - Object[] params = (Object[]) tag; - boolean download = (Boolean) params[0]; - AttachmentView attachment = (AttachmentView) params[1]; - if (download) { - attachment.writeFile(); - } else { - attachment.showFile(); - } - } - }); - } - - @Override - public void loadAttachmentFailed(Account account, Message message, Part part, Object tag, String reason) { - if (mMessage != message) { - return; - } - mHandler.post(new Runnable() { - @Override - public void run() { - mMessageView.setAttachmentsEnabled(true); - removeDialog(R.id.dialog_attachment_progress); - mHandler.networkError(); - } - }); - } - } - /** * Used by MessageOpenPgpView */ @@ -668,7 +507,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener, PgpData data = new PgpData(); data.setDecryptedData(decryptedData); data.setSignatureResult(signatureResult); - mMessageView.setMessage(mAccount, messageViewInfo, data, mController, mListener); + mMessageView.setMessage(mAccount, messageViewInfo, data); } catch (MessagingException e) { Log.e(K9.LOG_TAG, "displayMessageBody failed", e); } @@ -860,4 +699,41 @@ public class MessageViewFragment extends Fragment implements OnClickListener, // Do nothing } } + + @Override + public void onViewAttachment(AttachmentViewInfo attachment) { + //TODO: check if we have to download the attachment first + + getAttachmentController(attachment).viewAttachment(); + } + + @Override + public void onSaveAttachment(AttachmentViewInfo attachment) { + //TODO: check if we have to download the attachment first + + getAttachmentController(attachment).saveAttachment(); + } + + @Override + public void onSaveAttachmentToUserProvidedDirectory(final AttachmentViewInfo attachment) { + //TODO: check if we have to download the attachment first + + currentAttachmentViewInfo = attachment; + FileBrowserHelper.getInstance().showFileBrowserActivity(MessageViewFragment.this, null, + ACTIVITY_CHOOSE_DIRECTORY, new FileBrowserFailOverCallback() { + @Override + public void onPathEntered(String path) { + getAttachmentController(attachment).saveAttachmentTo(path); + } + + @Override + public void onCancel() { + // Do nothing + } + }); + } + + private AttachmentController getAttachmentController(AttachmentViewInfo attachment) { + return new AttachmentController(mMessageView, attachment); + } } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java index 586da1a9a..33086ea04 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java @@ -5,6 +5,8 @@ import java.io.FileOutputStream; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; +import java.util.HashMap; +import java.util.Map; import android.app.Activity; import android.app.Fragment; @@ -39,8 +41,6 @@ import android.widget.Toast; import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.R; -import com.fsck.k9.controller.MessagingController; -import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.crypto.PgpData; import com.fsck.k9.helper.ClipboardManager; import com.fsck.k9.helper.Contacts; @@ -52,12 +52,11 @@ import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mailstore.AttachmentViewInfo; import com.fsck.k9.mailstore.MessageViewInfo; import com.fsck.k9.provider.AttachmentProvider.AttachmentProviderColumns; -import com.fsck.k9.ui.messageview.AttachmentView.AttachmentFileDownloadCallback; import com.fsck.k9.view.MessageHeader; import com.fsck.k9.view.MessageHeader.OnLayoutChangedListener; import com.fsck.k9.view.MessageWebView; @@ -103,11 +102,12 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, private Button mDownloadRemainder; private LayoutInflater mInflater; private Contacts mContacts; - private AttachmentFileDownloadCallback attachmentCallback; + private AttachmentViewCallback attachmentCallback; private View mAttachmentsContainer; private SavedState mSavedState; private ClipboardManager mClipboardManager; private String mText; + private Map attachments = new HashMap(); public void initialize(Fragment fragment, OnClickListener flagListener) { @@ -497,8 +497,8 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, return mHeaderContainer.additionalHeadersVisible(); } - public void setMessage(Account account, MessageViewInfo messageViewInfo, PgpData pgpData, - MessagingController controller, MessagingListener listener) throws MessagingException { + public void setMessage(Account account, MessageViewInfo messageViewInfo, PgpData pgpData) + throws MessagingException { resetView(); String text = null; @@ -517,9 +517,8 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, mText = text; mHasAttachments = !messageViewInfo.attachments.isEmpty(); - if (mHasAttachments) { - renderAttachments(messageViewInfo, account, controller, listener); + renderAttachments(messageViewInfo); } mHiddenAttachments.setVisibility(View.GONE); @@ -593,30 +592,15 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, mMessageContentView.setVisibility(show ? View.VISIBLE : View.GONE); } - public void setAttachmentsEnabled(boolean enabled) { - for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { - AttachmentView attachment = (AttachmentView) mAttachments.getChildAt(i); - attachment.setButtonsEnabled(enabled); - } - } - - public void removeAllAttachments() { - for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { - mAttachments.removeView(mAttachments.getChildAt(i)); - } - } - - public void renderAttachments(MessageViewInfo messageContainer, Account account, MessagingController controller, - MessagingListener listener) throws MessagingException { - - for (Part attachment : messageContainer.attachments) { + public void renderAttachments(MessageViewInfo messageContainer) throws MessagingException { + for (AttachmentViewInfo attachment : messageContainer.attachments) { AttachmentView view = (AttachmentView) mInflater.inflate(R.layout.message_view_attachment, null); view.setCallback(attachmentCallback); + view.setAttachment(attachment); - boolean isFirstClassAttachment = view.populateFromPart(attachment, messageContainer.message, account, - controller, listener); + attachments.put(attachment, view); - if (isFirstClassAttachment) { + if (attachment.firstClassAttachment) { addAttachment(view); } else { addHiddenAttachment(view); @@ -667,12 +651,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, mHeaderContainer.setVisibility(View.GONE); } - public AttachmentView.AttachmentFileDownloadCallback getAttachmentCallback() { - return attachmentCallback; - } - - public void setAttachmentCallback( - AttachmentView.AttachmentFileDownloadCallback attachmentCallback) { + public void setAttachmentCallback(AttachmentViewCallback attachmentCallback) { this.attachmentCallback = attachmentCallback; } @@ -711,6 +690,18 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, } } + public void enableAttachmentViewButton(AttachmentViewInfo attachment) { + getAttachmentView(attachment).enableViewButton(); + } + + public void disableAttachmentViewButton(AttachmentViewInfo attachment) { + getAttachmentView(attachment).disableViewButton(); + } + + private AttachmentView getAttachmentView(AttachmentViewInfo attachment) { + return attachments.get(attachment); + } + static class SavedState extends BaseSavedState { boolean attachmentViewVisible; boolean hiddenAttachmentsVisible; From 41bd420213b1ca6b1b66f435322d297714966f26 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 15 Jan 2015 23:35:50 +0100 Subject: [PATCH 023/152] Include database ID in message parts reconstructed from the database --- .../com/fsck/k9/mailstore/LocalBodyPart.java | 27 +++++++++++++++++++ .../com/fsck/k9/mailstore/LocalFolder.java | 2 +- .../java/com/fsck/k9/mailstore/LocalPart.java | 7 +++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java create mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java new file mode 100644 index 000000000..3ea9820a6 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java @@ -0,0 +1,27 @@ +package com.fsck.k9.mailstore; + + +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.internet.MimeBodyPart; + + +public class LocalBodyPart extends MimeBodyPart implements LocalPart { + private final String accountUuid; + private final long messagePartId; + + public LocalBodyPart(String accountUuid, long messagePartId) throws MessagingException { + super(); + this.accountUuid = accountUuid; + this.messagePartId = messagePartId; + } + + @Override + public String getAccountUuid() { + return accountUuid; + } + + @Override + public long getId() { + return messagePartId; + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index d15fdec2e..708681241 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -686,7 +686,7 @@ public class LocalFolder extends Folder implements Serializable { String parentMimeType = parentPart.getMimeType(); if (parentMimeType.startsWith("multipart/")) { - BodyPart bodyPart = new MimeBodyPart(); + BodyPart bodyPart = new LocalBodyPart(getAccountUuid(), id); ((Multipart) parentPart.getBody()).addBodyPart(bodyPart); part = bodyPart; } else if (parentMimeType.startsWith("message/")) { diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java new file mode 100644 index 000000000..239595716 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java @@ -0,0 +1,7 @@ +package com.fsck.k9.mailstore; + + +public interface LocalPart { + String getAccountUuid(); + long getId(); +} From ac365567ee84801ee86e07617c50ebd40baa0c5d Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 16 Jan 2015 00:12:47 +0100 Subject: [PATCH 024/152] Replace dummy URI in AttachmentViewInfo instances This is a first step towards fixing viewing of attachments. --- .../java/com/fsck/k9/mailstore/LocalMessageExtractor.java | 6 +++++- .../main/java/com/fsck/k9/provider/AttachmentProvider.java | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java index fc867b61a..a7d5bc59b 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java @@ -16,6 +16,7 @@ import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.Viewable; +import com.fsck.k9.provider.AttachmentProvider; import java.util.ArrayList; import java.util.Date; @@ -495,7 +496,10 @@ public class LocalMessageExtractor { } catch (NumberFormatException e) { /* ignore */ } } - Uri uri = Uri.parse("dummy://this.needs.fixing"); //FIXME + LocalPart localPart = (LocalPart) part; + String accountUuid = localPart.getAccountUuid(); + long messagePartId = localPart.getId(); + Uri uri = AttachmentProvider.getAttachmentUri(accountUuid, messagePartId); return new AttachmentViewInfo(mimeType, name, size, uri, firstClassAttachment, part); } diff --git a/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java b/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java index 579861b6b..8a996e729 100644 --- a/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java +++ b/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java @@ -52,9 +52,9 @@ public class AttachmentProvider extends ContentProvider { } - public static Uri getAttachmentUri(Account account, long id) { + public static Uri getAttachmentUri(String accountUuid, long id) { return CONTENT_URI.buildUpon() - .appendPath(account.getUuid()) + .appendPath(accountUuid) .appendPath(Long.toString(id)) .appendPath(FORMAT_RAW) .build(); From cb94b5b192c392a6a7b3950781db2790ae30ad7b Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 16 Jan 2015 05:05:11 +0100 Subject: [PATCH 025/152] Store attachment information in LocalBodyPart --- .../fsck/k9/mailstore/AttachmentViewInfo.java | 2 + .../com/fsck/k9/mailstore/LocalBodyPart.java | 24 ++++++++++- .../com/fsck/k9/mailstore/LocalFolder.java | 42 ++++++++++++++----- .../k9/mailstore/LocalMessageExtractor.java | 24 ++++++++--- .../java/com/fsck/k9/mailstore/LocalPart.java | 3 ++ 5 files changed, 77 insertions(+), 18 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java b/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java index 8b0bb3050..f969a3c00 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java @@ -7,6 +7,8 @@ import com.fsck.k9.mail.Part; public class AttachmentViewInfo { + public static final long UNKNOWN_SIZE = -1; + public final String mimeType; public final String displayName; public final long size; diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java index 3ea9820a6..ebab22c60 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java @@ -8,11 +8,18 @@ import com.fsck.k9.mail.internet.MimeBodyPart; public class LocalBodyPart extends MimeBodyPart implements LocalPart { private final String accountUuid; private final long messagePartId; + private final String displayName; + private final long size; + private final boolean firstClassAttachment; - public LocalBodyPart(String accountUuid, long messagePartId) throws MessagingException { + public LocalBodyPart(String accountUuid, long messagePartId, String displayName, long size, + boolean firstClassAttachment) throws MessagingException { super(); this.accountUuid = accountUuid; this.messagePartId = messagePartId; + this.displayName = displayName; + this.size = size; + this.firstClassAttachment = firstClassAttachment; } @Override @@ -24,4 +31,19 @@ public class LocalBodyPart extends MimeBodyPart implements LocalPart { public long getId() { return messagePartId; } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public long getSize() { + return size; + } + + @Override + public boolean isFirstClassAttachment() { + return firstClassAttachment; + } } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 708681241..1d2b8c3b9 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -24,6 +24,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; import android.util.Log; import com.fsck.k9.Account; @@ -42,7 +43,6 @@ import com.fsck.k9.mail.MessageRetrievalListener; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; -import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMultipart; @@ -61,6 +61,7 @@ import org.apache.james.mime4j.util.MimeUtil; public class LocalFolder extends Folder implements Serializable { private static final long serialVersionUID = -1973296520918624767L; + private static final Uri PLACEHOLDER_URI = Uri.EMPTY; private final LocalStore localStore; @@ -669,11 +670,15 @@ public class LocalFolder extends Folder implements Serializable { throws MessagingException { long id = cursor.getLong(0); + int type = cursor.getInt(1); long parentId = cursor.getLong(2); String mimeType = cursor.getString(3); + long size = cursor.getLong(4); + String displayName = cursor.getString(5); byte[] header = cursor.getBlob(6); int dataLocation = cursor.getInt(9); String serverExtra = cursor.getString(15); + boolean firstClassAttachment = (type != MessagePartType.HIDDEN_ATTACHMENT); final Part part; if (id == message.getMessagePartId()) { @@ -686,7 +691,7 @@ public class LocalFolder extends Folder implements Serializable { String parentMimeType = parentPart.getMimeType(); if (parentMimeType.startsWith("multipart/")) { - BodyPart bodyPart = new LocalBodyPart(getAccountUuid(), id); + BodyPart bodyPart = new LocalBodyPart(getAccountUuid(), id, displayName, size, firstClassAttachment); ((Multipart) parentPart.getBody()).addBodyPart(bodyPart); part = bodyPart; } else if (parentMimeType.startsWith("message/")) { @@ -1353,18 +1358,32 @@ public class LocalFolder extends Folder implements Serializable { cv.put("preamble", multipart.getPreamble()); cv.put("epilogue", multipart.getEpilogue()); cv.put("boundary", multipart.getBoundary()); - } else if (body == null) { - //TODO: deal with missing parts - cv.put("data_location", DataLocation.MISSING); } else { - cv.put("data_location", DataLocation.IN_DATABASE); + AttachmentViewInfo attachment = LocalMessageExtractor.extractAttachmentInfo(part, PLACEHOLDER_URI); - byte[] bodyData = getBodyBytes(body); - String encoding = getTransferEncoding(part); + cv.put("display_name", attachment.displayName); - cv.put("encoding", encoding); - cv.put("data", bodyData); - cv.put("content_id", part.getContentId()); + if (body == null) { + //TODO: deal with missing parts + cv.put("data_location", DataLocation.MISSING); + cv.put("decoded_body_size", attachment.size); + } else { + cv.put("data_location", DataLocation.IN_DATABASE); + + byte[] bodyData = getBodyBytes(body); + String encoding = getTransferEncoding(part); + + if (attachment.size == AttachmentViewInfo.UNKNOWN_SIZE) { + //FIXME: Use size of content when transfer encoding is stripped + cv.put("decoded_body_size", bodyData.length); + } else { + cv.put("decoded_body_size", attachment.size); + } + + cv.put("encoding", encoding); + cv.put("data", bodyData); + cv.put("content_id", part.getContentId()); + } } } @@ -1941,6 +1960,7 @@ public class LocalFolder extends Folder implements Serializable { static final int TEXT = 3; static final int RELATED = 4; static final int ATTACHMENT = 5; + static final int HIDDEN_ATTACHMENT = 6; } // Note: The contents of the 'message_parts' table depend on these values. diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java index a7d5bc59b..f0ab12dd7 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java @@ -462,6 +462,23 @@ public class LocalMessageExtractor { } private static AttachmentViewInfo extractAttachmentInfo(Part part) throws MessagingException { + if (part instanceof LocalPart) { + LocalPart localPart = (LocalPart) part; + String accountUuid = localPart.getAccountUuid(); + long messagePartId = localPart.getId(); + String mimeType = part.getMimeType(); + String displayName = localPart.getDisplayName(); + long size = localPart.getSize(); + boolean firstClassAttachment = localPart.isFirstClassAttachment(); + Uri uri = AttachmentProvider.getAttachmentUri(accountUuid, messagePartId); + + return new AttachmentViewInfo(mimeType, displayName, size, uri, firstClassAttachment, part); + } else { + throw new IllegalStateException("Not supported yet"); + } + } + + public static AttachmentViewInfo extractAttachmentInfo(Part part, Uri uri) throws MessagingException { boolean firstClassAttachment = true; String mimeType = part.getMimeType(); @@ -488,7 +505,7 @@ public class LocalMessageExtractor { firstClassAttachment = false; } - long size = 0; + long size = AttachmentViewInfo.UNKNOWN_SIZE; String sizeParam = MimeUtility.getHeaderParameter(contentDisposition, "size"); if (sizeParam != null) { try { @@ -496,11 +513,6 @@ public class LocalMessageExtractor { } catch (NumberFormatException e) { /* ignore */ } } - LocalPart localPart = (LocalPart) part; - String accountUuid = localPart.getAccountUuid(); - long messagePartId = localPart.getId(); - Uri uri = AttachmentProvider.getAttachmentUri(accountUuid, messagePartId); - return new AttachmentViewInfo(mimeType, name, size, uri, firstClassAttachment, part); } } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java index 239595716..6fb9c50fd 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java @@ -4,4 +4,7 @@ package com.fsck.k9.mailstore; public interface LocalPart { String getAccountUuid(); long getId(); + String getDisplayName(); + long getSize(); + boolean isFirstClassAttachment(); } From de2eb25446590a2c6e00a6f78dd7b05521d5e0ff Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 16 Jan 2015 17:38:31 +0100 Subject: [PATCH 026/152] Use Glide for thumbnail generation + image loading --- k9mail/build.gradle | 1 + .../java/com/fsck/k9/activity/Accounts.java | 3 +- .../com/fsck/k9/mailstore/LocalFolder.java | 20 -- .../k9/mailstore/StoreSchemaDefinition.java | 2 - .../fsck/k9/provider/AttachmentProvider.java | 185 ++---------------- .../k9/ui/messageview/AttachmentView.java | 50 +---- 6 files changed, 26 insertions(+), 235 deletions(-) diff --git a/k9mail/build.gradle b/k9mail/build.gradle index c99ed2f0c..66f07b201 100644 --- a/k9mail/build.gradle +++ b/k9mail/build.gradle @@ -17,6 +17,7 @@ dependencies { compile 'com.android.support:support-v13:21.0.2' compile 'net.sourceforge.htmlcleaner:htmlcleaner:2.10' compile 'de.cketti.library.changelog:ckchangelog:1.2.1' + compile 'com.github.bumptech.glide:glide:3.4.0' androidTestCompile 'com.android.support.test:testing-support-lib:0.1' androidTestCompile 'com.android.support.test.espresso:espresso-core:2.0' diff --git a/k9mail/src/main/java/com/fsck/k9/activity/Accounts.java b/k9mail/src/main/java/com/fsck/k9/activity/Accounts.java index f60311eea..0a83aa9ec 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/Accounts.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/Accounts.java @@ -1284,7 +1284,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { new String[] {"HtmlCleaner", "http://htmlcleaner.sourceforge.net/"}, new String[] {"Android-PullToRefresh", "https://github.com/chrisbanes/Android-PullToRefresh"}, new String[] {"ckChangeLog", "https://github.com/cketti/ckChangeLog"}, - new String[] {"HoloColorPicker", "https://github.com/LarsWerkman/HoloColorPicker"} + new String[] {"HoloColorPicker", "https://github.com/LarsWerkman/HoloColorPicker"}, + new String[] {"Glide", "https://github.com/bumptech/glide"} }; private void onAbout() { diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 1d2b8c3b9..0d90f09a4 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -20,7 +20,6 @@ import java.util.Stack; import java.util.UUID; import android.content.ContentValues; -import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; @@ -48,7 +47,6 @@ import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.WrappedException; -import com.fsck.k9.provider.AttachmentProvider; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.parser.ContentHandler; import org.apache.james.mime4j.parser.MimeStreamParser; @@ -1651,7 +1649,6 @@ public class LocalFolder extends Folder implements Serializable { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { deleteMessagePartsFromDisk(db, rootMessagePartId); - deleteAttachmentThumbnailsFromDisk(db, rootMessagePartId); return null; } }); @@ -1679,23 +1676,6 @@ public class LocalFolder extends Folder implements Serializable { } } - private void deleteAttachmentThumbnailsFromDisk(SQLiteDatabase db, long rootMessagePartId) { - Context context = localStore.context; - String accountUuid = getAccountUuid(); - - Cursor cursor = db.query("message_parts", new String[] { "id" }, - "root = ? AND type = " + MessagePartType.ATTACHMENT, - new String[] { Long.toString(rootMessagePartId) }, null, null, null); - try { - while (cursor.moveToNext()) { - String messagePartId = cursor.getString(0); - AttachmentProvider.deleteThumbnail(context, accountUuid, messagePartId); - } - } finally { - cursor.close(); - } - } - @Override public boolean isInTopGroup() { return mInTopGroup; diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java b/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java index d8be088ee..e57930a65 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java @@ -53,8 +53,6 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition { Log.i(K9.LOG_TAG, String.format(Locale.US, "Upgrading database from version %d to version %d", db.getVersion(), LocalStore.DB_VERSION)); - AttachmentProvider.clear(this.localStore.context); - db.beginTransaction(); try { // schema version 29 was when we moved to incremental updates diff --git a/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java b/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java index 8a996e729..0d1f0adad 100644 --- a/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java +++ b/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java @@ -1,13 +1,15 @@ package com.fsck.k9.provider; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + import android.content.ContentProvider; import android.content.ContentValues; -import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.util.Log; @@ -16,14 +18,10 @@ import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.LocalStore; import com.fsck.k9.mailstore.LocalStore.AttachmentInfo; import org.openintents.openpgp.util.ParcelFileDescriptorUtil; -import java.io.*; -import java.util.List; - /** * A simple ContentProvider that allows file access to attachments. @@ -37,7 +35,6 @@ public class AttachmentProvider extends ContentProvider { private static final String FORMAT_RAW = "RAW"; private static final String FORMAT_VIEW = "VIEW"; - private static final String FORMAT_THUMBNAIL = "THUMBNAIL"; private static final String[] DEFAULT_PROJECTION = new String[] { AttachmentProviderColumns._ID, @@ -60,86 +57,8 @@ public class AttachmentProvider extends ContentProvider { .build(); } - public static Uri getAttachmentUriForViewing(Account account, long id, String mimeType, String filename) { - return CONTENT_URI.buildUpon() - .appendPath(account.getUuid()) - .appendPath(Long.toString(id)) - .appendPath(FORMAT_VIEW) - .appendPath(mimeType) - .appendPath(filename) - .build(); - } - - public static Uri getAttachmentThumbnailUri(Account account, long id, int width, int height) { - return CONTENT_URI.buildUpon() - .appendPath(account.getUuid()) - .appendPath(Long.toString(id)) - .appendPath(FORMAT_THUMBNAIL) - .appendPath(Integer.toString(width)) - .appendPath(Integer.toString(height)) - .build(); - } - - public static void clear(Context context) { - /* - * We use the cache dir as a temporary directory (since Android doesn't give us one) so - * on startup we'll clean up any .tmp files from the last run. - */ - File[] files = context.getCacheDir().listFiles(); - for (File file : files) { - try { - if (K9.DEBUG) { - Log.d(K9.LOG_TAG, "Deleting file " + file.getCanonicalPath()); - } - } catch (IOException ioe) { /* No need to log failure to log */ } - file.delete(); - } - } - - /** - * Delete the thumbnail of an attachment. - * - * @param context - * The application context. - * @param accountUuid - * The UUID of the account the attachment belongs to. - * @param attachmentId - * The ID of the attachment the thumbnail was created for. - */ - public static void deleteThumbnail(Context context, String accountUuid, String attachmentId) { - File file = getThumbnailFile(context, accountUuid, attachmentId); - if (file.exists()) { - file.delete(); - } - } - - private static File getThumbnailFile(Context context, String accountUuid, String attachmentId) { - String filename = "thmb_" + accountUuid + "_" + attachmentId + ".tmp"; - File dir = context.getCacheDir(); - return new File(dir, filename); - } - - @Override public boolean onCreate() { - /* - * We use the cache dir as a temporary directory (since Android doesn't give us one) so - * on startup we'll clean up any .tmp files from the last run. - */ - final File cacheDir = getContext().getCacheDir(); - if (cacheDir == null) { - return true; - } - File[] files = cacheDir.listFiles(); - if (files == null) { - return true; - } - for (File file : files) { - if (file.getName().endsWith(".tmp")) { - file.delete(); - } - } - return true; } @@ -159,14 +78,6 @@ public class AttachmentProvider extends ContentProvider { List segments = uri.getPathSegments(); String accountUuid = segments.get(0); String attachmentId = segments.get(1); - String format = segments.get(2); - - if (FORMAT_THUMBNAIL.equals(format)) { - int width = Integer.parseInt(segments.get(3)); - int height = Integer.parseInt(segments.get(4)); - - return openThumbnail(accountUuid, attachmentId, width, height); - } return openAttachment(accountUuid, attachmentId); } @@ -231,66 +142,25 @@ public class AttachmentProvider extends ContentProvider { private String getType(String accountUuid, String id, String format, String mimeType) { String type; - if (FORMAT_THUMBNAIL.equals(format)) { - type = "image/png"; - } else { - final Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid); + final Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid); - try { - final LocalStore localStore = LocalStore.getInstance(account, getContext()); + try { + final LocalStore localStore = LocalStore.getInstance(account, getContext()); - AttachmentInfo attachmentInfo = localStore.getAttachmentInfo(id); - if (FORMAT_VIEW.equals(format) && mimeType != null) { - type = mimeType; - } else { - type = attachmentInfo.type; - } - } catch (MessagingException e) { - Log.e(K9.LOG_TAG, "Unable to retrieve LocalStore for " + account, e); - type = null; + AttachmentInfo attachmentInfo = localStore.getAttachmentInfo(id); + if (FORMAT_VIEW.equals(format) && mimeType != null) { + type = mimeType; + } else { + type = attachmentInfo.type; } + } catch (MessagingException e) { + Log.e(K9.LOG_TAG, "Unable to retrieve LocalStore for " + account, e); + type = null; } return type; } - private ParcelFileDescriptor openThumbnail(String accountUuid, String attachmentId, int width, int height) - throws FileNotFoundException { - - File file = getThumbnailFile(getContext(), accountUuid, attachmentId); - if (!file.exists()) { - String type = getType(accountUuid, attachmentId, FORMAT_VIEW, null); - try { - InputStream in = getAttachmentInputStream(accountUuid, attachmentId); - try { - Bitmap thumbnail = createThumbnail(type, in); - if (thumbnail != null) { - thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true); - FileOutputStream out = new FileOutputStream(file); - try { - thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out); - } finally { - try { - out.close(); - } catch (IOException e) { - Log.e(K9.LOG_TAG, "Error saving thumbnail", e); - } - } - } - } finally { - try { - in.close(); - } catch (Throwable ignore) { /* ignore */ } - } - } catch (MessagingException e) { - Log.e(K9.LOG_TAG, "Error getting InputStream for attachment", e); - return null; - } - } - - return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); - } - private ParcelFileDescriptor openAttachment(String accountUuid, String attachmentId) { try { InputStream inputStream = getAttachmentInputStream(accountUuid, attachmentId); @@ -309,27 +179,4 @@ public class AttachmentProvider extends ContentProvider { LocalStore localStore = LocalStore.getInstance(account, getContext()); return localStore.getAttachmentInputStream(attachmentId); } - - private Bitmap createThumbnail(String type, InputStream data) { - if (MimeUtility.mimeTypeMatches(type, "image/*")) { - return createImageThumbnail(data); - } - return null; - } - - private Bitmap createImageThumbnail(InputStream data) { - try { - return BitmapFactory.decodeStream(data); - } catch (OutOfMemoryError oome) { - /* - * Improperly downloaded images, corrupt bitmaps and the like can commonly - * cause OOME due to invalid allocation sizes. We're happy with a null bitmap in - * that case. If the system is really out of memory we'll know about it soon - * enough. - */ - return null; - } catch (Exception e) { - return null; - } - } } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java index ac2acb59b..b93bb453a 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java @@ -2,8 +2,6 @@ package com.fsck.k9.ui.messageview; import android.content.Context; -import android.graphics.Bitmap; -import android.os.AsyncTask; import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; @@ -13,6 +11,7 @@ import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; +import com.bumptech.glide.Glide; import com.fsck.k9.K9; import com.fsck.k9.R; import com.fsck.k9.helper.SizeFormatter; @@ -76,8 +75,12 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo attachmentName.setText(attachment.displayName); attachmentInfo.setText(SizeFormatter.formatSize(getContext(), attachment.size)); - ImageView thumbnail = (ImageView) findViewById(R.id.attachment_icon); - new LoadAndDisplayThumbnailAsyncTask(thumbnail).execute(); + ImageView thumbnailView = (ImageView) findViewById(R.id.attachment_icon); + Glide.with(getContext()) + .load(attachment.uri) + .placeholder(R.drawable.attached_image_placeholder) + .centerCrop() + .into(thumbnailView); } @Override @@ -119,43 +122,4 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo public void setCallback(AttachmentViewCallback callback) { this.callback = callback; } - - private class LoadAndDisplayThumbnailAsyncTask extends AsyncTask { - private final ImageView thumbnail; - - public LoadAndDisplayThumbnailAsyncTask(ImageView thumbnail) { - this.thumbnail = thumbnail; - } - - protected Bitmap doInBackground(Void... asyncTaskArgs) { - return getPreviewIcon(); - } - - private Bitmap getPreviewIcon() { - //FIXME - temporarily disabled - return null; -// Bitmap icon = null; -// try { -// InputStream input = context.getContentResolver().openInputStream( -// AttachmentProvider.getAttachmentThumbnailUri(account, -// part.getAttachmentId(), -// 62, -// 62)); -// icon = BitmapFactory.decodeStream(input); -// input.close(); -// } catch (Exception e) { -// // We don't care what happened, we just return null for the preview icon. -// } -// -// return icon; - } - - protected void onPostExecute(Bitmap previewIcon) { - if (previewIcon != null) { - thumbnail.setImageBitmap(previewIcon); - } else { - thumbnail.setImageResource(R.drawable.attached_image_placeholder); - } - } - } } From 658657447e42ad09e45d833d4825310509be6773 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 16 Jan 2015 18:18:48 +0100 Subject: [PATCH 027/152] Fix viewing attachment with alternative MIME type --- .../fsck/k9/mailstore/AttachmentViewInfo.java | 8 ++++++++ .../fsck/k9/provider/AttachmentProvider.java | 20 ++++++------------- .../ui/messageview/AttachmentController.java | 14 ++++++++++++- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java b/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java index f969a3c00..88d1e1981 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java @@ -12,6 +12,14 @@ public class AttachmentViewInfo { public final String mimeType; public final String displayName; public final long size; + + /** + * A content provider URI that can be used to retrieve the decoded attachment. + *

+ * Note: All content providers must support an alternative MIME type appended as last URI segment. + * + * @see com.fsck.k9.ui.messageview.AttachmentController#getAttachmentUriForMimeType(AttachmentViewInfo, String) + */ public final Uri uri; public final boolean firstClassAttachment; public final Part part; diff --git a/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java b/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java index 0d1f0adad..a458ccf5a 100644 --- a/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java +++ b/k9mail/src/main/java/com/fsck/k9/provider/AttachmentProvider.java @@ -18,6 +18,7 @@ import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.LocalStore; import com.fsck.k9.mailstore.LocalStore.AttachmentInfo; import org.openintents.openpgp.util.ParcelFileDescriptorUtil; @@ -25,17 +26,10 @@ import org.openintents.openpgp.util.ParcelFileDescriptorUtil; /** * A simple ContentProvider that allows file access to attachments. - *

- * Warning! We make heavy assumptions about the Uris used by the {@link LocalStore} for an - * {@link Account} here. - *

*/ public class AttachmentProvider extends ContentProvider { public static final Uri CONTENT_URI = Uri.parse("content://com.fsck.k9.attachmentprovider"); - private static final String FORMAT_RAW = "RAW"; - private static final String FORMAT_VIEW = "VIEW"; - private static final String[] DEFAULT_PROJECTION = new String[] { AttachmentProviderColumns._ID, AttachmentProviderColumns.DATA, @@ -53,7 +47,6 @@ public class AttachmentProvider extends ContentProvider { return CONTENT_URI.buildUpon() .appendPath(accountUuid) .appendPath(Long.toString(id)) - .appendPath(FORMAT_RAW) .build(); } @@ -67,10 +60,9 @@ public class AttachmentProvider extends ContentProvider { List segments = uri.getPathSegments(); String accountUuid = segments.get(0); String id = segments.get(1); - String format = segments.get(2); - String mimeType = (segments.size() < 4) ? null : segments.get(3); + String mimeType = (segments.size() < 3) ? null : segments.get(2); - return getType(accountUuid, id, format, mimeType); + return getType(accountUuid, id, mimeType); } @Override @@ -140,7 +132,7 @@ public class AttachmentProvider extends ContentProvider { return null; } - private String getType(String accountUuid, String id, String format, String mimeType) { + private String getType(String accountUuid, String id, String mimeType) { String type; final Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid); @@ -148,14 +140,14 @@ public class AttachmentProvider extends ContentProvider { final LocalStore localStore = LocalStore.getInstance(account, getContext()); AttachmentInfo attachmentInfo = localStore.getAttachmentInfo(id); - if (FORMAT_VIEW.equals(format) && mimeType != null) { + if (mimeType != null) { type = mimeType; } else { type = attachmentInfo.type; } } catch (MessagingException e) { Log.e(K9.LOG_TAG, "Unable to retrieve LocalStore for " + account, e); - type = null; + type = MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE; } return type; diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java index fe8cd0f8a..36c2c8be7 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java @@ -158,14 +158,26 @@ public class AttachmentController { } private Intent createViewIntentForAttachmentProviderUri(String mimeType) { + Uri uri = getAttachmentUriForMimeType(attachment, mimeType); + Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(attachment.uri, mimeType); + intent.setDataAndType(uri, mimeType); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); addUiIntentFlags(intent); return intent; } + private Uri getAttachmentUriForMimeType(AttachmentViewInfo attachment, String mimeType) { + if (attachment.mimeType.equals(mimeType)) { + return attachment.uri; + } + + return attachment.uri.buildUpon() + .appendPath(mimeType) + .build(); + } + private Intent createViewIntentForFileUri(String mimeType, Uri uri) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uri, mimeType); From 1e628e7177eab73a46bd3327550828009121e5cf Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 16 Jan 2015 20:19:21 +0100 Subject: [PATCH 028/152] Reduce exposure of SingleMessageView internals --- .../ui/messageview/MessageViewFragment.java | 21 +++++++++---------- .../k9/ui/messageview/SingleMessageView.java | 19 +++++++++++------ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java index 719708e1a..40a1175c8 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java @@ -48,7 +48,7 @@ import com.fsck.k9.view.MessageHeader; import org.openintents.openpgp.OpenPgpSignatureResult; -public class MessageViewFragment extends Fragment implements OnClickListener, ConfirmationDialogFragmentListener, +public class MessageViewFragment extends Fragment implements ConfirmationDialogFragmentListener, AttachmentViewCallback { private static final String ARG_REFERENCE = "reference"; @@ -144,13 +144,19 @@ public class MessageViewFragment extends Fragment implements OnClickListener, Co mMessageView.setAttachmentCallback(this); - mMessageView.initialize(this, new OnClickListener() { + mMessageView.initialize(this); + mMessageView.setOnToggleFlagClickListener(new OnClickListener() { @Override public void onClick(View v) { onToggleFlagged(); } }); - mMessageView.downloadRemainderButton().setOnClickListener(this); + mMessageView.setOnDownloadButtonClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onDownloadRemainder(); + } + }); mFragmentListener.messageHeaderViewAvailable(mMessageView.getMessageHeaderView()); @@ -455,18 +461,11 @@ public class MessageViewFragment extends Fragment implements OnClickListener, Co if (mMessage.isSet(Flag.X_DOWNLOADED_FULL)) { return; } - mMessageView.downloadRemainderButton().setEnabled(false); + mMessageView.disableDownloadButton(); //FIXME // mController.loadMessageForViewRemote(mAccount, mMessageReference.folderName, mMessageReference.uid, mListener); } - @Override - public void onClick(View view) { - if (view.getId() == R.id.download_remainder) { - onDownloadRemainder(); - } - } - private void setProgress(boolean enable) { if (mFragmentListener != null) { mFragmentListener.setProgress(enable); diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java index 33086ea04..1997c36fa 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java @@ -110,7 +110,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, private Map attachments = new HashMap(); - public void initialize(Fragment fragment, OnClickListener flagListener) { + public void initialize(Fragment fragment) { Activity activity = fragment.getActivity(); mMessageContentView = (MessageWebView) findViewById(R.id.message_content); mMessageContentView.configure(); @@ -119,7 +119,6 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, mHeaderContainer = (MessageHeader) findViewById(R.id.header_container); mHeaderContainer.setOnLayoutChangedListener(this); - mHeaderContainer.setOnFlagListener(flagListener); mAttachmentsContainer = findViewById(R.id.attachments_container); mAttachments = (LinearLayout) findViewById(R.id.attachments); @@ -446,10 +445,6 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, showShowPicturesAction(false); } - public Button downloadRemainderButton() { - return mDownloadRemainder; - } - public void showShowPicturesAction(boolean show) { mShowPicturesAction.setVisibility(show ? View.VISIBLE : View.GONE); } @@ -480,6 +475,18 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, } } + public void setOnToggleFlagClickListener(OnClickListener listener) { + mHeaderContainer.setOnFlagListener(listener); + } + + public void setOnDownloadButtonClickListener(OnClickListener listener) { + mDownloadRemainder.setOnClickListener(listener); + } + + public void disableDownloadButton() { + mDownloadRemainder.setEnabled(false); + } + public void setShowDownloadButton(Message message) { if (message.isSet(Flag.X_DOWNLOADED_FULL)) { mDownloadRemainder.setVisibility(View.GONE); From 585d9cbe7f8a5702c8c7e9048d0c7daeb303de7e Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 16 Jan 2015 23:33:54 +0100 Subject: [PATCH 029/152] Fix "Download complete message" --- .../com/fsck/k9/activity/MessageCompose.java | 2 +- .../fsck/k9/controller/MessagingListener.java | 2 +- .../ui/messageview/MessageViewFragment.java | 53 ++++++++++++++++++- .../k9/ui/messageview/SingleMessageView.java | 4 ++ 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java b/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java index 8fff3fbb0..e6f2f856d 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java @@ -3402,7 +3402,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, } @Override - public void loadMessageForViewFinished(Account account, String folder, String uid, Message message) { + public void loadMessageForViewFinished(Account account, String folder, String uid, LocalMessage message) { if ((mMessageReference == null) || !mMessageReference.uid.equals(uid)) { return; } diff --git a/k9mail/src/main/java/com/fsck/k9/controller/MessagingListener.java b/k9mail/src/main/java/com/fsck/k9/controller/MessagingListener.java index 72bd93d18..9190fe422 100644 --- a/k9mail/src/main/java/com/fsck/k9/controller/MessagingListener.java +++ b/k9mail/src/main/java/com/fsck/k9/controller/MessagingListener.java @@ -91,7 +91,7 @@ public class MessagingListener { Message message) {} public void loadMessageForViewFinished(Account account, String folder, String uid, - Message message) {} + LocalMessage message) {} public void loadMessageForViewFailed(Account account, String folder, String uid, Throwable t) {} diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java index 40a1175c8..c91033128 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java @@ -8,12 +8,14 @@ import android.app.Activity; import android.app.DialogFragment; import android.app.Fragment; import android.app.FragmentManager; +import android.app.LoaderManager; import android.app.LoaderManager.LoaderCallbacks; import android.content.Context; import android.content.Intent; import android.content.Loader; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; import android.text.TextUtils; import android.util.Log; import android.view.ContextThemeWrapper; @@ -31,6 +33,7 @@ import com.fsck.k9.R; import com.fsck.k9.activity.ChooseFolder; import com.fsck.k9.activity.MessageReference; import com.fsck.k9.controller.MessagingController; +import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.crypto.PgpData; import com.fsck.k9.fragment.ConfirmationDialogFragment; import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener; @@ -82,6 +85,8 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF private LocalMessage mMessage; private MessagingController mController; private LayoutInflater mLayoutInflater; + private Handler handler = new Handler(); + private DownloadMessageListener downloadMessageListener = new DownloadMessageListener(); /** * Used to temporarily store the destination folder for refile operations if a confirmation @@ -231,6 +236,27 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF throw new RuntimeException("Not implemented yet"); } + private void onMessageDownloadFinished(LocalMessage message) { + mMessage = message; + + LoaderManager loaderManager = getLoaderManager(); + loaderManager.destroyLoader(LOCAL_MESSAGE_LOADER_ID); + loaderManager.destroyLoader(DECODE_MESSAGE_LOADER_ID); + + onLoadMessageFromDatabaseFinished(mMessage); + } + + private void onDownloadMessageFailed(Throwable t) { + mMessageView.enableDownloadButton(); + String errorMessage; + if (t instanceof IllegalArgumentException) { + errorMessage = mContext.getString(R.string.status_invalid_id_error); + } else { + errorMessage = mContext.getString(R.string.status_network_error); + } + Toast.makeText(mContext, errorMessage, Toast.LENGTH_LONG).show(); + } + private void startExtractingTextAndAttachments(LocalMessage message) { getLoaderManager().initLoader(DECODE_MESSAGE_LOADER_ID, null, decodeMessageLoaderCallback); } @@ -462,8 +488,9 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF return; } mMessageView.disableDownloadButton(); - //FIXME -// mController.loadMessageForViewRemote(mAccount, mMessageReference.folderName, mMessageReference.uid, mListener); + + mController.loadMessageForViewRemote(mAccount, mMessageReference.folderName, mMessageReference.uid, + downloadMessageListener); } private void setProgress(boolean enable) { @@ -735,4 +762,26 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF private AttachmentController getAttachmentController(AttachmentViewInfo attachment) { return new AttachmentController(mMessageView, attachment); } + + private class DownloadMessageListener extends MessagingListener { + @Override + public void loadMessageForViewFinished(Account account, String folder, String uid, final LocalMessage message) { + handler.post(new Runnable() { + @Override + public void run() { + onMessageDownloadFinished(message); + } + }); + } + + @Override + public void loadMessageForViewFailed(Account account, String folder, String uid, final Throwable t) { + handler.post(new Runnable() { + @Override + public void run() { + onDownloadMessageFailed(t); + } + }); + } + } } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java index 1997c36fa..a7f2abebf 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java @@ -483,6 +483,10 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, mDownloadRemainder.setOnClickListener(listener); } + public void enableDownloadButton() { + mDownloadRemainder.setEnabled(true); + } + public void disableDownloadButton() { mDownloadRemainder.setEnabled(false); } From 9363c5b276ed806fc316b06ea01ba3dbee1b541f Mon Sep 17 00:00:00 2001 From: cketti Date: Sun, 18 Jan 2015 03:35:28 +0100 Subject: [PATCH 030/152] Download missing parts before viewing or saving --- .../k9/controller/MessagingController.java | 57 +++--------- .../fsck/k9/controller/MessagingListener.java | 8 +- .../com/fsck/k9/mailstore/LocalBodyPart.java | 9 +- .../com/fsck/k9/mailstore/LocalFolder.java | 3 +- .../java/com/fsck/k9/mailstore/LocalPart.java | 1 + .../ui/messageview/AttachmentController.java | 93 +++++++++++++++++-- .../k9/ui/messageview/AttachmentView.java | 6 +- .../ui/messageview/MessageViewFragment.java | 33 ++++++- .../k9/ui/messageview/SingleMessageView.java | 20 +++- 9 files changed, 161 insertions(+), 69 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java b/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java index 8b0cd1495..d0e339e1b 100644 --- a/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -3144,40 +3144,8 @@ public class MessagingController implements Runnable { } } - /** - * Attempts to load the attachment specified by part from the given account and message. - * @param account - * @param message - * @param part - * @param listener - */ - public void loadAttachment( - final Account account, - final Message message, - final Part part, - final Object tag, - final MessagingListener listener) { - /* - * Check if the attachment has already been downloaded. If it has there's no reason to - * download it, so we just tell the listener that it's ready to go. - */ - - if (part.getBody() != null) { - for (MessagingListener l : getListeners(listener)) { - l.loadAttachmentStarted(account, message, part, tag, false); - } - - for (MessagingListener l : getListeners(listener)) { - l.loadAttachmentFinished(account, message, part, tag); - } - return; - } - - - - for (MessagingListener l : getListeners(listener)) { - l.loadAttachmentStarted(account, message, part, tag, true); - } + public void loadAttachment(final Account account, final LocalMessage message, final Part part, + final MessagingListener listener) { put("loadAttachment", listener, new Runnable() { @Override @@ -3185,32 +3153,29 @@ public class MessagingController implements Runnable { Folder remoteFolder = null; LocalFolder localFolder = null; try { - LocalStore localStore = account.getLocalStore(); + String folderName = message.getFolder().getName(); + + LocalStore localStore = account.getLocalStore(); + localFolder = localStore.getFolder(folderName); - List attachments = MessageExtractor.collectAttachments(message); - for (Part attachment : attachments) { - attachment.setBody(null); - } Store remoteStore = account.getRemoteStore(); - localFolder = localStore.getFolder(message.getFolder().getName()); - remoteFolder = remoteStore.getFolder(message.getFolder().getName()); + remoteFolder = remoteStore.getFolder(folderName); remoteFolder.open(Folder.OPEN_MODE_RW); - //FIXME: This is an ugly hack that won't be needed once the Message objects have been united. Message remoteMessage = remoteFolder.getMessage(message.getUid()); - MimeMessageHelper.setBody(remoteMessage, message.getBody()); remoteFolder.fetchPart(remoteMessage, part, null); - localFolder.addPartToMessage((LocalMessage) message, part); + localFolder.addPartToMessage(message, part); + for (MessagingListener l : getListeners(listener)) { - l.loadAttachmentFinished(account, message, part, tag); + l.loadAttachmentFinished(account, message, part); } } catch (MessagingException me) { if (K9.DEBUG) Log.v(K9.LOG_TAG, "Exception loading attachment", me); for (MessagingListener l : getListeners(listener)) { - l.loadAttachmentFailed(account, message, part, tag, me.getMessage()); + l.loadAttachmentFailed(account, message, part, me.getMessage()); } notifyUserIfCertificateProblem(context, me, account, true); addErrorMessage(account, null, me); diff --git a/k9mail/src/main/java/com/fsck/k9/controller/MessagingListener.java b/k9mail/src/main/java/com/fsck/k9/controller/MessagingListener.java index 9190fe422..5b6970d38 100644 --- a/k9mail/src/main/java/com/fsck/k9/controller/MessagingListener.java +++ b/k9mail/src/main/java/com/fsck/k9/controller/MessagingListener.java @@ -133,13 +133,9 @@ public class MessagingListener { public void setPushActive(Account account, String folderName, boolean enabled) {} - public void loadAttachmentStarted(Account account, Message message, Part part, Object tag, - boolean requiresDownload) {} + public void loadAttachmentFinished(Account account, Message message, Part part) {} - public void loadAttachmentFinished(Account account, Message message, Part part, Object tag) {} - - public void loadAttachmentFailed(Account account, Message message, Part part, Object tag, - String reason) {} + public void loadAttachmentFailed(Account account, Message message, Part part, String reason) {} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java index ebab22c60..ede69874d 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java @@ -7,15 +7,17 @@ import com.fsck.k9.mail.internet.MimeBodyPart; public class LocalBodyPart extends MimeBodyPart implements LocalPart { private final String accountUuid; + private final LocalMessage message; private final long messagePartId; private final String displayName; private final long size; private final boolean firstClassAttachment; - public LocalBodyPart(String accountUuid, long messagePartId, String displayName, long size, + public LocalBodyPart(String accountUuid, LocalMessage message, long messagePartId, String displayName, long size, boolean firstClassAttachment) throws MessagingException { super(); this.accountUuid = accountUuid; + this.message = message; this.messagePartId = messagePartId; this.displayName = displayName; this.size = size; @@ -46,4 +48,9 @@ public class LocalBodyPart extends MimeBodyPart implements LocalPart { public boolean isFirstClassAttachment() { return firstClassAttachment; } + + @Override + public LocalMessage getMessage() { + return message; + } } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 0d90f09a4..61e1b6ff0 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -689,7 +689,8 @@ public class LocalFolder extends Folder implements Serializable { String parentMimeType = parentPart.getMimeType(); if (parentMimeType.startsWith("multipart/")) { - BodyPart bodyPart = new LocalBodyPart(getAccountUuid(), id, displayName, size, firstClassAttachment); + BodyPart bodyPart = new LocalBodyPart(getAccountUuid(), message, id, displayName, size, + firstClassAttachment); ((Multipart) parentPart.getBody()).addBodyPart(bodyPart); part = bodyPart; } else if (parentMimeType.startsWith("message/")) { diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java index 6fb9c50fd..134212812 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java @@ -7,4 +7,5 @@ public interface LocalPart { String getDisplayName(); long getSize(); boolean isFirstClassAttachment(); + LocalMessage getMessage(); } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java index 36c2c8be7..b5f2021ee 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java @@ -19,29 +19,44 @@ import android.os.Environment; import android.util.Log; import android.widget.Toast; +import com.fsck.k9.Account; import com.fsck.k9.K9; +import com.fsck.k9.Preferences; import com.fsck.k9.R; import com.fsck.k9.cache.TemporaryAttachmentStore; +import com.fsck.k9.controller.MessagingController; +import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.helper.FileHelper; import com.fsck.k9.helper.MediaScannerNotifier; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.AttachmentViewInfo; +import com.fsck.k9.mailstore.LocalMessage; +import com.fsck.k9.mailstore.LocalPart; import org.apache.commons.io.IOUtils; public class AttachmentController { private final Context context; - private final SingleMessageView messageView; + private final MessagingController controller; + private final MessageViewFragment messageViewFragment; private final AttachmentViewInfo attachment; - AttachmentController(SingleMessageView messageView, AttachmentViewInfo attachment) { - this.context = messageView.getContext(); - this.messageView = messageView; + AttachmentController(MessagingController controller, MessageViewFragment messageViewFragment, + AttachmentViewInfo attachment) { + this.context = messageViewFragment.getContext(); + this.controller = controller; + this.messageViewFragment = messageViewFragment; this.attachment = attachment; } public void viewAttachment() { - new ViewAttachmentAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + if (needsDownloading()) { + downloadAndViewAttachment((LocalPart) attachment.part); + } else { + viewLocalAttachment(); + } } public void saveAttachment() { @@ -52,6 +67,60 @@ public class AttachmentController { saveAttachmentTo(new File(directory)); } + private boolean needsDownloading() { + return isPartMissing() && isLocalPart(); + } + + private boolean isPartMissing() { + return attachment.part.getBody() == null; + } + + private boolean isLocalPart() { + return attachment.part instanceof LocalPart; + } + + private void downloadAndViewAttachment(LocalPart localPart) { + downloadAttachment(localPart, new Runnable() { + @Override + public void run() { + viewLocalAttachment(); + } + }); + } + + private void downloadAndSaveAttachmentTo(LocalPart localPart, final File directory) { + downloadAttachment(localPart, new Runnable() { + @Override + public void run() { + saveAttachmentTo(directory); + } + }); + } + + private void downloadAttachment(LocalPart localPart, final Runnable attachmentDownloadedCallback) { + String accountUuid = localPart.getAccountUuid(); + Account account = Preferences.getPreferences(context).getAccount(accountUuid); + LocalMessage message = localPart.getMessage(); + + messageViewFragment.showAttachmentLoadingDialog(); + controller.loadAttachment(account, message, attachment.part, new MessagingListener() { + @Override + public void loadAttachmentFinished(Account account, Message message, Part part) { + messageViewFragment.hideAttachmentLoadingDialogOnMainThread(); + messageViewFragment.runOnMainThread(attachmentDownloadedCallback); + } + + @Override + public void loadAttachmentFailed(Account account, Message message, Part part, String reason) { + messageViewFragment.hideAttachmentLoadingDialogOnMainThread(); + } + }); + } + + private void viewLocalAttachment() { + new ViewAttachmentAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + private void saveAttachmentTo(File directory) { boolean isExternalStorageMounted = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); if (!isExternalStorageMounted) { @@ -60,6 +129,14 @@ public class AttachmentController { return; } + if (needsDownloading()) { + downloadAndSaveAttachmentTo((LocalPart) attachment.part, directory); + } else { + saveLocalAttachmentTo(directory); + } + } + + private void saveLocalAttachmentTo(File directory) { //FIXME: write file in background thread try { File file = saveAttachmentWithUniqueFileName(directory); @@ -187,7 +264,7 @@ public class AttachmentController { } private void addUiIntentFlags(Intent intent) { - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); } private int getResolvedIntentActivitiesCount(Intent intent) { @@ -243,7 +320,7 @@ public class AttachmentController { @Override protected void onPreExecute() { - messageView.disableAttachmentViewButton(attachment); + messageViewFragment.disableAttachmentButtons(attachment); } @Override @@ -254,7 +331,7 @@ public class AttachmentController { @Override protected void onPostExecute(Intent intent) { viewAttachment(intent); - messageView.enableAttachmentViewButton(attachment); + messageViewFragment.enableAttachmentButtons(attachment); } private void viewAttachment(Intent intent) { diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java index b93bb453a..2713da359 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java @@ -43,12 +43,14 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo return attachment; } - public void enableViewButton() { + public void enableButtons() { viewButton.setEnabled(true); + downloadButton.setEnabled(true); } - public void disableViewButton() { + public void disableButtons() { viewButton.setEnabled(false); + downloadButton.setEnabled(false); } public void setAttachment(AttachmentViewInfo attachment) throws MessagingException { diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java index c91033128..67657f1c6 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java @@ -663,6 +663,37 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF } } + public Context getContext() { + return mContext; + } + + public void disableAttachmentButtons(AttachmentViewInfo attachment) { + mMessageView.disableAttachmentButtons(attachment); + } + + public void enableAttachmentButtons(AttachmentViewInfo attachment) { + mMessageView.enableAttachmentButtons(attachment); + } + + public void runOnMainThread(Runnable runnable) { + handler.post(runnable); + } + + public void showAttachmentLoadingDialog() { + mMessageView.disableAttachmentButtons(); + showDialog(R.id.dialog_attachment_progress); + } + + public void hideAttachmentLoadingDialogOnMainThread() { + handler.post(new Runnable() { + @Override + public void run() { + removeDialog(R.id.dialog_attachment_progress); + mMessageView.enableAttachmentButtons(); + } + }); + } + public interface MessageViewFragmentListener { public void onForward(LocalMessage mMessage, PgpData mPgpData); public void disableDeleteAction(); @@ -760,7 +791,7 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF } private AttachmentController getAttachmentController(AttachmentViewInfo attachment) { - return new AttachmentController(mMessageView, attachment); + return new AttachmentController(mController, this, attachment); } private class DownloadMessageListener extends MessagingListener { diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java index a7f2abebf..433d0911b 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java @@ -500,6 +500,18 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, } } + public void enableAttachmentButtons() { + for (AttachmentView attachmentView : attachments.values()) { + attachmentView.enableButtons(); + } + } + + public void disableAttachmentButtons() { + for (AttachmentView attachmentView : attachments.values()) { + attachmentView.disableButtons(); + } + } + public void showAllHeaders() { mHeaderContainer.onShowAdditionalHeaders(); } @@ -701,12 +713,12 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, } } - public void enableAttachmentViewButton(AttachmentViewInfo attachment) { - getAttachmentView(attachment).enableViewButton(); + public void enableAttachmentButtons(AttachmentViewInfo attachment) { + getAttachmentView(attachment).enableButtons(); } - public void disableAttachmentViewButton(AttachmentViewInfo attachment) { - getAttachmentView(attachment).disableViewButton(); + public void disableAttachmentButtons(AttachmentViewInfo attachment) { + getAttachmentView(attachment).disableButtons(); } private AttachmentView getAttachmentView(AttachmentViewInfo attachment) { From a7b16c1210d7562208dfd65c029df30b6d9c4ed3 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 19 Jan 2015 23:08:41 +0100 Subject: [PATCH 031/152] Refresh thumbnail after downloading attachment --- .../k9/ui/messageview/AttachmentController.java | 1 + .../fsck/k9/ui/messageview/AttachmentView.java | 16 ++++++++++------ .../k9/ui/messageview/MessageViewFragment.java | 4 ++++ .../k9/ui/messageview/SingleMessageView.java | 4 ++++ 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java index b5f2021ee..9af2e764d 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java @@ -92,6 +92,7 @@ public class AttachmentController { downloadAttachment(localPart, new Runnable() { @Override public void run() { + messageViewFragment.refreshAttachmentThumbnail(attachment); saveAttachmentTo(directory); } }); diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java index 2713da359..ff8992ad5 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentView.java @@ -77,12 +77,7 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo attachmentName.setText(attachment.displayName); attachmentInfo.setText(SizeFormatter.formatSize(getContext(), attachment.size)); - ImageView thumbnailView = (ImageView) findViewById(R.id.attachment_icon); - Glide.with(getContext()) - .load(attachment.uri) - .placeholder(R.drawable.attached_image_placeholder) - .centerCrop() - .into(thumbnailView); + refreshThumbnail(); } @Override @@ -124,4 +119,13 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo public void setCallback(AttachmentViewCallback callback) { this.callback = callback; } + + public void refreshThumbnail() { + ImageView thumbnailView = (ImageView) findViewById(R.id.attachment_icon); + Glide.with(getContext()) + .load(attachment.uri) + .placeholder(R.drawable.attached_image_placeholder) + .centerCrop() + .into(thumbnailView); + } } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java index 67657f1c6..61ff0c9be 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java @@ -694,6 +694,10 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF }); } + public void refreshAttachmentThumbnail(AttachmentViewInfo attachment) { + mMessageView.refreshAttachmentThumbnail(attachment); + } + public interface MessageViewFragmentListener { public void onForward(LocalMessage mMessage, PgpData mPgpData); public void disableDeleteAction(); diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java index 433d0911b..ac9ed9953 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java @@ -721,6 +721,10 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, getAttachmentView(attachment).disableButtons(); } + public void refreshAttachmentThumbnail(AttachmentViewInfo attachment) { + getAttachmentView(attachment).refreshThumbnail(); + } + private AttachmentView getAttachmentView(AttachmentViewInfo attachment) { return attachments.get(attachment); } From da51bdf1b34f49acf1188acdbda654e4b893df2c Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 19 Jan 2015 23:35:14 +0100 Subject: [PATCH 032/152] Save attachments in background thread --- .../ui/messageview/AttachmentController.java | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java index 9af2e764d..5ee4dbcee 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java @@ -138,19 +138,7 @@ public class AttachmentController { } private void saveLocalAttachmentTo(File directory) { - //FIXME: write file in background thread - try { - File file = saveAttachmentWithUniqueFileName(directory); - - displayAttachmentSavedMessage(file.toString()); - - MediaScannerNotifier.notify(context, file); - } catch (IOException ioe) { - if (K9.DEBUG) { - Log.e(K9.LOG_TAG, "Error saving attachment", ioe); - } - displayAttachmentNotSavedMessage(); - } + new SaveAttachmentAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, directory); } private File saveAttachmentWithUniqueFileName(File directory) throws IOException { @@ -346,4 +334,36 @@ public class AttachmentController { } } } + + private class SaveAttachmentAsyncTask extends AsyncTask { + + @Override + protected void onPreExecute() { + messageViewFragment.disableAttachmentButtons(attachment); + } + + @Override + protected File doInBackground(File... params) { + try { + File directory = params[0]; + return saveAttachmentWithUniqueFileName(directory); + } catch (IOException e) { + if (K9.DEBUG) { + Log.e(K9.LOG_TAG, "Error saving attachment", e); + } + return null; + } + } + + @Override + protected void onPostExecute(File file) { + messageViewFragment.enableAttachmentButtons(attachment); + if (file != null) { + displayAttachmentSavedMessage(file.toString()); + MediaScannerNotifier.notify(context, file); + } else { + displayAttachmentNotSavedMessage(); + } + } + } } From 395b70fa22736972d129ee842b9df194272dbe7b Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 20 Jan 2015 16:37:30 +0100 Subject: [PATCH 033/152] Remove unused code --- .../k9/mailstore/LocalMessageExtractor.java | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java index f0ab12dd7..137435394 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java @@ -5,15 +5,12 @@ import android.net.Uri; import com.fsck.k9.R; import com.fsck.k9.mail.Address; -import com.fsck.k9.mail.Body; -import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.mail.internet.MessageExtractor; import com.fsck.k9.mail.internet.MimeHeader; -import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.Viewable; import com.fsck.k9.provider.AttachmentProvider; @@ -414,35 +411,6 @@ public class LocalMessageExtractor { html.append(""); } - private static ViewableContainer extractTextual(Part part) throws MessagingException { - String text = ""; - String html = ""; - List attachments = new ArrayList(); - - Body firstBody = part.getBody(); - if (part.isMimeType("text/plain")) { - String bodyText = MessageExtractor.getTextFromPart(part); - if (bodyText != null) { - text = bodyText; - html = HtmlConverter.textToHtml(text); - } - } else if (part.isMimeType("multipart/alternative") && - firstBody instanceof MimeMultipart) { - MimeMultipart multipart = (MimeMultipart) firstBody; - for (BodyPart bodyPart : multipart.getBodyParts()) { - String bodyText = MessageExtractor.getTextFromPart(bodyPart); - if (bodyText != null) { - if (text.isEmpty() && bodyPart.isMimeType("text/plain")) { - text = bodyText; - } else if (html.isEmpty() && bodyPart.isMimeType("text/html")) { - html = bodyText; - } - } - } - } - return new ViewableContainer(text, html, attachments); - } - public static MessageViewInfo decodeMessageForView(Context context, Message message) throws MessagingException { //TODO: Modify extractTextAndAttachments() to only extract the text type (plain vs. HTML) we currently need. ViewableContainer viewable = LocalMessageExtractor.extractTextAndAttachments(context, message); From c9b2ec533ce56760dece466ccf57e250a5ebffcc Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 21 Jan 2015 00:56:52 +0100 Subject: [PATCH 034/152] Add MessagePreviewExtractor --- .../main/java/com/fsck/k9/mail/Message.java | 49 ----- .../MessagePreviewExtractorTest.java | 141 +++++++++++++++ .../k9/mailstore/MessagePreviewExtractor.java | 168 ++++++++++++++++++ k9mail/src/main/res/values/strings.xml | 3 + 4 files changed, 312 insertions(+), 49 deletions(-) create mode 100644 k9mail/src/androidTest/java/com/fsck/k9/mailstore/MessagePreviewExtractorTest.java create mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/MessagePreviewExtractor.java diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java index a77e0c282..46ff8302c 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java @@ -150,55 +150,6 @@ public abstract class Message implements Part, CompositeBody { public abstract int getSize(); - /* - * calculateContentPreview - * Takes a plain text message body as a string. - * Returns a message summary as a string suitable for showing in a message list - * - * A message summary should be about the first 160 characters - * of unique text written by the message sender - * Quoted text, "On $date" and so on will be stripped out. - * All newlines and whitespace will be compressed. - * - */ - public static String calculateContentPreview(String text) { - if (text == null) { - return null; - } - - // Only look at the first 8k of a message when calculating - // the preview. This should avoid unnecessary - // memory usage on large messages - if (text.length() > 8192) { - text = text.substring(0, 8192); - } - - // Remove (correctly delimited by '-- \n') signatures - text = text.replaceAll("(?ms)^-- [\\r\\n]+.*", ""); - // try to remove lines of dashes in the preview - text = text.replaceAll("(?m)^----.*?$", ""); - // remove quoted text from the preview - text = text.replaceAll("(?m)^[#>].*$", ""); - // Remove a common quote header from the preview - text = text.replaceAll("(?m)^On .*wrote.?$", ""); - // Remove a more generic quote header from the preview - text = text.replaceAll("(?m)^.*\\w+:$", ""); - // Remove horizontal rules. - text = text.replaceAll("\\s*([-=_]{30,}+)\\s*", " "); - - // URLs in the preview should just be shown as "..." - They're not - // clickable and they usually overwhelm the preview - text = text.replaceAll("https?://\\S+", "..."); - // Don't show newlines in the preview - text = text.replaceAll("(\\r|\\n)+", " "); - // Collapse whitespace in the preview - text = text.replaceAll("\\s+", " "); - // Remove any whitespace at the beginning and end of the string. - text = text.trim(); - - return (text.length() <= 512) ? text : text.substring(0, 512); - } - public void delete(String trashFolderName) throws MessagingException {} /* diff --git a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/MessagePreviewExtractorTest.java b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/MessagePreviewExtractorTest.java new file mode 100644 index 000000000..dcbd437cc --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/MessagePreviewExtractorTest.java @@ -0,0 +1,141 @@ +package com.fsck.k9.mailstore; + + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.internet.MimeBodyPart; +import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.TextBody; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; + + +@RunWith(AndroidJUnit4.class) +public class MessagePreviewExtractorTest { + + @Test + public void shouldExtractPreviewFromSinglePlainTextPart() throws MessagingException { + MimeMessage message = new MimeMessage(); + message.addHeader("Content-Type", "text/plain"); + TextBody body = new TextBody("Message text "); + message.setBody(body); + + String preview = MessagePreviewExtractor.extractPreview(getContext(), message); + + assertEquals("Message text", preview); + } + + @Test + public void shouldLimitPreviewTo512Characters() throws MessagingException { + MimeMessage message = new MimeMessage(); + message.addHeader("Content-Type", "text/plain"); + TextBody body = new TextBody("10--------20--------30--------40--------50--------" + + "60--------70--------80--------90--------100-------" + + "110-------120-------130-------140-------150-------" + + "160-------170-------180-------190-------200-------" + + "210-------220-------230-------240-------250-------" + + "260-------270-------280-------290-------300-------" + + "310-------320-------330-------340-------350-------" + + "360-------370-------380-------390-------400-------" + + "410-------420-------430-------440-------450-------" + + "460-------470-------480-------490-------500-------" + + "510-------520-------530-------540-------550-------" + + "560-------570-------580-------590-------600-------"); + message.setBody(body); + + String preview = MessagePreviewExtractor.extractPreview(getContext(), message); + + assertEquals(512, preview.length()); + assertEquals('…', preview.charAt(511)); + } + + @Test + public void shouldExtractPreviewFromSingleHtmlPart() throws MessagingException { + MimeMessage message = new MimeMessage(); + message.addHeader("Content-Type", "text/html"); + TextBody body = new TextBody("
Message text
"); + message.setBody(body); + + String preview = MessagePreviewExtractor.extractPreview(getContext(), message); + + assertEquals("Message text", preview); + } + + @Test + public void shouldExtractPreviewFromMultipartAlternative() throws MessagingException { + MimeMessage message = new MimeMessage(); + message.addHeader("Content-Type", "multipart/alternative"); + MimeMultipart multipart = new MimeMultipart(); + multipart.setSubType("alternative"); + message.setBody(multipart); + + TextBody textBody = new TextBody("text"); + MimeBodyPart textPart = new MimeBodyPart(textBody, "text/plain"); + multipart.addBodyPart(textPart); + + TextBody htmlBody = new TextBody("html"); + MimeBodyPart htmlPart = new MimeBodyPart(htmlBody, "text/html"); + multipart.addBodyPart(htmlPart); + + String preview = MessagePreviewExtractor.extractPreview(getContext(), message); + + assertEquals("text", preview); + } + + @Test + public void shouldExtractPreviewFromMultipartMixed() throws MessagingException { + MimeMessage message = new MimeMessage(); + message.addHeader("Content-Type", "multipart/mixed"); + MimeMultipart multipart = new MimeMultipart(); + multipart.setSubType("mixed"); + message.setBody(multipart); + + TextBody textBody = new TextBody("text"); + MimeBodyPart textPart = new MimeBodyPart(textBody, "text/plain"); + multipart.addBodyPart(textPart); + + TextBody htmlBody = new TextBody("html"); + MimeBodyPart htmlPart = new MimeBodyPart(htmlBody, "text/html"); + multipart.addBodyPart(htmlPart); + + String preview = MessagePreviewExtractor.extractPreview(getContext(), message); + + assertEquals("text / html", preview); + } + + @Test + public void shouldExtractPreviewFromMultipartMixedWithInnerMesssage() throws MessagingException { + MimeMessage message = new MimeMessage(); + message.addHeader("Content-Type", "multipart/mixed"); + MimeMultipart multipart = new MimeMultipart(); + multipart.setSubType("mixed"); + message.setBody(multipart); + + TextBody textBody = new TextBody("text"); + MimeBodyPart textPart = new MimeBodyPart(textBody, "text/plain"); + multipart.addBodyPart(textPart); + + MimeMessage innerMessage = new MimeMessage(); + innerMessage.addHeader("Content-Type", "text/html"); + innerMessage.addHeader("Subject", "inner message"); + TextBody htmlBody = new TextBody("html"); + innerMessage.setBody(htmlBody); + + MimeBodyPart messagePart = new MimeBodyPart(innerMessage, "message/rfc822"); + multipart.addBodyPart(messagePart); + + String preview = MessagePreviewExtractor.extractPreview(getContext(), message); + + assertEquals("text / Includes message titled \"inner message\" containing: html", preview); + } + + private Context getContext() { + return InstrumentationRegistry.getTargetContext(); + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/MessagePreviewExtractor.java b/k9mail/src/main/java/com/fsck/k9/mailstore/MessagePreviewExtractor.java new file mode 100644 index 000000000..60be77618 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/MessagePreviewExtractor.java @@ -0,0 +1,168 @@ +package com.fsck.k9.mailstore; + + +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import android.text.TextUtils; + +import com.fsck.k9.R; +import com.fsck.k9.helper.HtmlConverter; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.internet.MessageExtractor; +import com.fsck.k9.mail.internet.Viewable; +import com.fsck.k9.mail.internet.Viewable.Alternative; +import com.fsck.k9.mail.internet.Viewable.Html; +import com.fsck.k9.mail.internet.Viewable.MessageHeader; +import com.fsck.k9.mail.internet.Viewable.Textual; + + +public class MessagePreviewExtractor { + private static final int MAX_PREVIEW_LENGTH = 512; + private static final int MAX_CHARACTERS_CHECKED_FOR_PREVIEW = 8192; + + public static String extractPreview(Context context, Message message) throws MessagingException { + try { + List attachments = new ArrayList(); + List viewables = MessageExtractor.getViewables(message, attachments); + + return buildPreview(context, viewables); + } catch (Exception e) { + throw new MessagingException("Couldn't extract viewable parts", e); + } + } + + private static String buildPreview(Context context, List viewables) throws MessagingException { + StringBuilder text = new StringBuilder(); + boolean divider = false; + + for (Viewable viewable : viewables) { + if (viewable instanceof Textual) { + appendText(text, viewable, divider); + divider = true; + } else if (viewable instanceof MessageHeader) { + appendMessagePreview(context, text, (MessageHeader) viewable, divider); + divider = false; + } else if (viewable instanceof Alternative) { + appendAlternative(text, (Alternative) viewable, divider); + divider = true; + } + + if (hasMaxPreviewLengthBeenReached(text)) { + break; + } + } + + if (hasMaxPreviewLengthBeenReached(text)) { + text.setLength(MAX_PREVIEW_LENGTH - 1); + text.append('…'); + } + + return text.toString(); + } + + private static void appendText(StringBuilder text, Viewable viewable, boolean prependDivider) { + if (viewable instanceof Textual) { + appendTextual(text, (Textual) viewable, prependDivider); + } else if (viewable instanceof Alternative) { + appendAlternative(text, (Alternative) viewable, prependDivider); + } else { + throw new IllegalArgumentException("Unknown Viewable"); + } + } + + private static void appendTextual(StringBuilder text, Textual textual, boolean prependDivider) { + Part part = textual.getPart(); + + if (prependDivider) { + appendDivider(text); + } + + String textFromPart = MessageExtractor.getTextFromPart(part); + if (textFromPart == null) { + textFromPart = ""; + } else if (textual instanceof Html) { + textFromPart = HtmlConverter.htmlToText(textFromPart); + } + + text.append(stripTextForPreview(textFromPart)); + } + + private static void appendAlternative(StringBuilder text, Alternative alternative, boolean prependDivider) { + List textAlternative = alternative.getText().isEmpty() ? + alternative.getHtml() : alternative.getText(); + + boolean divider = prependDivider; + for (Viewable textViewable : textAlternative) { + appendText(text, textViewable, divider); + divider = true; + + if (hasMaxPreviewLengthBeenReached(text)) { + break; + } + } + } + + private static void appendMessagePreview(Context context, StringBuilder text, MessageHeader messageHeader, + boolean divider) { + if (divider) { + appendDivider(text); + } + + String subject = messageHeader.getMessage().getSubject(); + if (TextUtils.isEmpty(subject)) { + text.append(context.getString(R.string.preview_untitled_inner_message)); + } else { + text.append(context.getString(R.string.preview_inner_message, subject)); + } + } + + private static void appendDivider(StringBuilder text) { + text.append(" / "); + } + + private static String stripTextForPreview(String text) { + if (text == null) { + return ""; + } + + // Only look at the first 8k of a message when calculating + // the preview. This should avoid unnecessary + // memory usage on large messages + if (text.length() > MAX_CHARACTERS_CHECKED_FOR_PREVIEW) { + text = text.substring(0, MAX_CHARACTERS_CHECKED_FOR_PREVIEW); + } + + // Remove (correctly delimited by '-- \n') signatures + text = text.replaceAll("(?ms)^-- [\\r\\n]+.*", ""); + // try to remove lines of dashes in the preview + text = text.replaceAll("(?m)^----.*?$", ""); + // remove quoted text from the preview + text = text.replaceAll("(?m)^[#>].*$", ""); + // Remove a common quote header from the preview + text = text.replaceAll("(?m)^On .*wrote.?$", ""); + // Remove a more generic quote header from the preview + text = text.replaceAll("(?m)^.*\\w+:$", ""); + // Remove horizontal rules. + text = text.replaceAll("\\s*([-=_]{30,}+)\\s*", " "); + + // URLs in the preview should just be shown as "..." - They're not + // clickable and they usually overwhelm the preview + text = text.replaceAll("https?://\\S+", "..."); + // Don't show newlines in the preview + text = text.replaceAll("(\\r|\\n)+", " "); + // Collapse whitespace in the preview + text = text.replaceAll("\\s+", " "); + // Remove any whitespace at the beginning and end of the string. + text = text.trim(); + + return (text.length() <= MAX_PREVIEW_LENGTH) ? text : text.substring(0, MAX_PREVIEW_LENGTH); + } + + private static boolean hasMaxPreviewLengthBeenReached(StringBuilder text) { + return text.length() >= MAX_PREVIEW_LENGTH; + } +} diff --git a/k9mail/src/main/res/values/strings.xml b/k9mail/src/main/res/values/strings.xml index 53c2699e4..b01c1a462 100644 --- a/k9mail/src/main/res/values/strings.xml +++ b/k9mail/src/main/res/values/strings.xml @@ -1124,4 +1124,7 @@ Please submit bug reports, contribute new features and ask questions at "Failed to retrieve client certificate for alias \"%s\"" Advanced options "Client certificate \"%1$s\" has expired or is not yet valid (%2$s)" + + "Includes message titled \"%s\" containing: " + "Includes untitled message containing: " From 64e92ab1c1d0b07492e77f6b62e860ac4c91bd9b Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 22 Jan 2015 04:12:05 +0100 Subject: [PATCH 035/152] Remove unused LocalAttachment* classes --- .../internet/BinaryTempFileMessageBody.java | 7 +-- .../com/fsck/k9/activity/MessageCompose.java | 25 +++++----- .../k9/mailstore/LocalAttachmentBody.java | 40 --------------- .../k9/mailstore/LocalAttachmentBodyPart.java | 31 ------------ .../mailstore/LocalAttachmentMessageBody.java | 49 ------------------- .../k9/mailstore/TempFileMessageBody.java | 10 ++-- 6 files changed, 22 insertions(+), 140 deletions(-) delete mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/LocalAttachmentBody.java delete mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/LocalAttachmentBodyPart.java delete mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/LocalAttachmentMessageBody.java diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java index e5fe7c370..a47f2dffd 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java @@ -11,10 +11,7 @@ import com.fsck.k9.mail.CompositeBody; import com.fsck.k9.mail.MessagingException; /** - * A {@link BinaryTempFileBody} extension containing a body of type - * message/rfc822. This relates to a BinaryTempFileBody the same way that a - * {@link LocalAttachmentMessageBody} relates to a {@link LocalAttachmentBody}. - * + * A {@link BinaryTempFileBody} extension containing a body of type message/rfc822. */ public class BinaryTempFileMessageBody extends BinaryTempFileBody implements CompositeBody { @@ -63,4 +60,4 @@ public class BinaryTempFileMessageBody extends BinaryTempFileBody implements Com */ } -} \ No newline at end of file +} diff --git a/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java b/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java index e6f2f856d..bc434105b 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java @@ -109,7 +109,6 @@ import com.fsck.k9.mail.internet.MimeMessageHelper; import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; -import com.fsck.k9.mailstore.LocalAttachmentBody; import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.TempFileBody; import com.fsck.k9.mailstore.TempFileMessageBody; @@ -2624,17 +2623,19 @@ public class MessageCompose extends K9Activity implements OnClickListener, String name = MimeUtility.getHeaderParameter(contentType, "name"); if (name != null) { Body body = part.getBody(); - if (body instanceof LocalAttachmentBody) { - final Uri uri = ((LocalAttachmentBody) body).getContentUri(); - mHandler.post(new Runnable() { - @Override - public void run() { - addAttachment(uri); - } - }); - } else { - return false; - } + //FIXME +// if (body instanceof LocalAttachmentBody) { +// final Uri uri = ((LocalAttachmentBody) body).getContentUri(); +// mHandler.post(new Runnable() { +// @Override +// public void run() { +// addAttachment(uri); +// } +// }); +// } else { +// return false; +// } + return false; } return true; } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalAttachmentBody.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalAttachmentBody.java deleted file mode 100644 index 0b191472a..000000000 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalAttachmentBody.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.fsck.k9.mailstore; - -import java.io.ByteArrayInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; - -import android.content.Context; -import android.net.Uri; - -import com.fsck.k9.mail.MessagingException; - -/** - * An attachment whose contents are loaded from an URI. - */ -public class LocalAttachmentBody extends BinaryAttachmentBody { - private Context context; - private Uri mUri; - - public LocalAttachmentBody(Uri uri, Context context) { - this.context = context; - mUri = uri; - } - - @Override - public InputStream getInputStream() throws MessagingException { - try { - return context.getContentResolver().openInputStream(mUri); - } catch (FileNotFoundException fnfe) { - /* - * Since it's completely normal for us to try to serve up attachments that - * have been blown away, we just return an empty stream. - */ - return new ByteArrayInputStream(LocalStore.EMPTY_BYTE_ARRAY); - } - } - - public Uri getContentUri() { - return mUri; - } -} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalAttachmentBodyPart.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalAttachmentBodyPart.java deleted file mode 100644 index ab6eae968..000000000 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalAttachmentBodyPart.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.fsck.k9.mailstore; - -import com.fsck.k9.mail.Body; -import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.internet.MimeBodyPart; - -public class LocalAttachmentBodyPart extends MimeBodyPart { - private long mAttachmentId = -1; - - public LocalAttachmentBodyPart(Body body, long attachmentId) throws MessagingException { - super(body); - mAttachmentId = attachmentId; - } - - /** - * Returns the local attachment id of this body, or -1 if it is not stored. - * @return - */ - public long getAttachmentId() { - return mAttachmentId; - } - - public void setAttachmentId(long attachmentId) { - mAttachmentId = attachmentId; - } - - @Override - public String toString() { - return "" + mAttachmentId; - } -} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalAttachmentMessageBody.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalAttachmentMessageBody.java deleted file mode 100644 index 82d62e092..000000000 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalAttachmentMessageBody.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.fsck.k9.mailstore; - -import java.io.IOException; -import java.io.OutputStream; - -import org.apache.james.mime4j.util.MimeUtil; - -import android.content.Context; -import android.net.Uri; - -import com.fsck.k9.mail.CompositeBody; -import com.fsck.k9.mail.MessagingException; - -/** - * A {@link LocalAttachmentBody} extension containing a message/rfc822 type body - * - */ -class LocalAttachmentMessageBody extends LocalAttachmentBody implements CompositeBody { - - public LocalAttachmentMessageBody(Uri uri, Context context) { - super(uri, context); - } - - @Override - public void writeTo(OutputStream out) throws IOException, MessagingException { - AttachmentMessageBodyUtil.writeTo(this, out); - } - - @Override - public void setUsing7bitTransport() throws MessagingException { - /* - * There's nothing to recurse into here, so there's nothing to do. - * The enclosing BodyPart already called setEncoding(MimeUtil.ENC_7BIT). Once - * writeTo() is called, the file with the rfc822 body will be opened - * for reading and will then be recursed. - */ - - } - - @Override - public void setEncoding(String encoding) throws MessagingException { - if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding) - && !MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) { - throw new MessagingException( - "Incompatible content-transfer-encoding applied to a CompositeBody"); - } - mEncoding = encoding; - } -} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/TempFileMessageBody.java b/k9mail/src/main/java/com/fsck/k9/mailstore/TempFileMessageBody.java index c310ae2e4..7730b20e5 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/TempFileMessageBody.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/TempFileMessageBody.java @@ -9,8 +9,7 @@ import com.fsck.k9.mail.CompositeBody; import com.fsck.k9.mail.MessagingException; /** - * An attachment containing a body of type message/rfc822 - * whose contents are contained in a file. + * An attachment containing a body of type message/rfc822 whose contents are contained in a file. */ public class TempFileMessageBody extends TempFileBody implements CompositeBody { @@ -25,7 +24,12 @@ public class TempFileMessageBody extends TempFileBody implements CompositeBody { @Override public void setUsing7bitTransport() throws MessagingException { - // see LocalAttachmentMessageBody.setUsing7bitTransport() + /* + * There's nothing to recurse into here, so there's nothing to do. + * The enclosing BodyPart already called setEncoding(MimeUtil.ENC_7BIT). Once + * writeTo() is called, the file with the rfc822 body will be opened + * for reading and will then be recursed. + */ } @Override From fe7b88f7c28b85555db491a859c5fc099699f20a Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 22 Jan 2015 04:56:08 +0100 Subject: [PATCH 036/152] Work around the BinaryTempFileBodyInputStream mess --- .../k9/mail/internet/MessageExtractor.java | 30 ++++--------------- .../fsck/k9/mail/internet/MimeUtility.java | 15 ++++++---- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java index 809be651b..f0ab570e5 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java @@ -48,7 +48,7 @@ public class MessageExtractor { * determine the charset from HTML message. */ if (mimeType.equalsIgnoreCase("text/html") && charset == null) { - InputStream in = part.getBody().getInputStream(); + InputStream in = MimeUtility.decodeBody(body); try { byte[] buf = new byte[256]; in.read(buf, 0, buf.length); @@ -64,18 +64,8 @@ public class MessageExtractor { } } finally { try { - if (in instanceof BinaryTempFileBody.BinaryTempFileBodyInputStream) { - /* - * If this is a BinaryTempFileBodyInputStream, calling close() - * will delete the file. But we can't let that happen because - * the file needs to be opened again by the code a few lines - * down. - */ - ((BinaryTempFileBody.BinaryTempFileBodyInputStream) in).closeWithoutDeleting(); - } else { - in.close(); - } - } catch (Exception e) { /* ignore */ } + MimeUtility.closeInputStreamWithoutDeletingTemporaryFiles(in); + } catch (IOException e) { /* ignore */ } } } charset = fixupCharset(charset, getMessageFromPart(part)); @@ -86,20 +76,10 @@ public class MessageExtractor { */ InputStream in = MimeUtility.decodeBody(body); try { - String text = CharsetSupport.readToString(in, charset); - - // Replace the body with a TextBody that already contains the decoded text - part.setBody(new TextBody(text)); - - return text; + return CharsetSupport.readToString(in, charset); } finally { try { - /* - * This time we don't care if it's a BinaryTempFileBodyInputStream. We - * replaced the body with a TextBody instance and hence don't need the - * file anymore. - */ - in.close(); + MimeUtility.closeInputStreamWithoutDeletingTemporaryFiles(in); } catch (IOException e) { /* Ignore */ } } } diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java index bfde9e0b4..ee68fe449 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java @@ -16,10 +16,7 @@ import org.apache.james.mime4j.util.MimeUtil; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; -import java.util.Set; import java.util.regex.Pattern; @@ -1029,7 +1026,7 @@ public class MimeUtility { @Override public void close() throws IOException { super.close(); - rawInputStream.close(); + closeInputStreamWithoutDeletingTemporaryFiles(rawInputStream); } }; } else if (MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(encoding)) { @@ -1037,7 +1034,7 @@ public class MimeUtility { @Override public void close() throws IOException { super.close(); - rawInputStream.close(); + closeInputStreamWithoutDeletingTemporaryFiles(rawInputStream); } }; } else { @@ -1050,6 +1047,14 @@ public class MimeUtility { return inputStream; } + public static void closeInputStreamWithoutDeletingTemporaryFiles(InputStream rawInputStream) throws IOException { + if (rawInputStream instanceof BinaryTempFileBody.BinaryTempFileBodyInputStream) { + ((BinaryTempFileBody.BinaryTempFileBodyInputStream) rawInputStream).closeWithoutDeleting(); + } else { + rawInputStream.close(); + } + } + public static String getMimeTypeByExtension(String filename) { String returnedType = null; String extension = null; From 5e4743bf666b5e6a6746cc273aa34575b3f7ef87 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 22 Jan 2015 05:18:50 +0100 Subject: [PATCH 037/152] Extract preview of message text --- k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 61e1b6ff0..df0f899bd 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -1247,10 +1247,11 @@ public class LocalFolder extends Folder implements Serializable { parentId = threadInfo.parentId; } - //TODO: construct message preview //TODO: get attachment count try { + String preview = MessagePreviewExtractor.extractPreview(localStore.context, message); + long rootMessagePartId = saveMessageParts(db, message); ContentValues cv = new ContentValues(); @@ -1270,7 +1271,7 @@ public class LocalFolder extends Folder implements Serializable { cv.put("to_list", Address.pack(message.getRecipients(RecipientType.TO))); cv.put("cc_list", Address.pack(message.getRecipients(RecipientType.CC))); cv.put("bcc_list", Address.pack(message.getRecipients(RecipientType.BCC))); - cv.put("preview", ""); //FIXME + cv.put("preview", preview); cv.put("reply_to_list", Address.pack(message.getReplyTo())); cv.put("attachment_count", 0); //FIXME cv.put("internal_date", message.getInternalDate() == null From d2d85393d38d85972cd0a190477024d359dbf368 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 22 Jan 2015 06:05:05 +0100 Subject: [PATCH 038/152] Save attachment count --- ...est.java => MessageInfoExtractorTest.java} | 14 +++--- .../com/fsck/k9/mailstore/LocalFolder.java | 8 ++-- .../k9/mailstore/MessageInfoExtractor.java | 43 +++++++++++++++++++ .../k9/mailstore/MessagePreviewExtractor.java | 18 +------- 4 files changed, 56 insertions(+), 27 deletions(-) rename k9mail/src/androidTest/java/com/fsck/k9/mailstore/{MessagePreviewExtractorTest.java => MessageInfoExtractorTest.java} (89%) create mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/MessageInfoExtractor.java diff --git a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/MessagePreviewExtractorTest.java b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/MessageInfoExtractorTest.java similarity index 89% rename from k9mail/src/androidTest/java/com/fsck/k9/mailstore/MessagePreviewExtractorTest.java rename to k9mail/src/androidTest/java/com/fsck/k9/mailstore/MessageInfoExtractorTest.java index dcbd437cc..3a7539240 100644 --- a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/MessagePreviewExtractorTest.java +++ b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/MessageInfoExtractorTest.java @@ -17,7 +17,7 @@ import static org.junit.Assert.assertEquals; @RunWith(AndroidJUnit4.class) -public class MessagePreviewExtractorTest { +public class MessageInfoExtractorTest { @Test public void shouldExtractPreviewFromSinglePlainTextPart() throws MessagingException { @@ -26,7 +26,7 @@ public class MessagePreviewExtractorTest { TextBody body = new TextBody("Message text "); message.setBody(body); - String preview = MessagePreviewExtractor.extractPreview(getContext(), message); + String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview(); assertEquals("Message text", preview); } @@ -49,7 +49,7 @@ public class MessagePreviewExtractorTest { "560-------570-------580-------590-------600-------"); message.setBody(body); - String preview = MessagePreviewExtractor.extractPreview(getContext(), message); + String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview(); assertEquals(512, preview.length()); assertEquals('…', preview.charAt(511)); @@ -62,7 +62,7 @@ public class MessagePreviewExtractorTest { TextBody body = new TextBody("
Message text
"); message.setBody(body); - String preview = MessagePreviewExtractor.extractPreview(getContext(), message); + String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview(); assertEquals("Message text", preview); } @@ -83,7 +83,7 @@ public class MessagePreviewExtractorTest { MimeBodyPart htmlPart = new MimeBodyPart(htmlBody, "text/html"); multipart.addBodyPart(htmlPart); - String preview = MessagePreviewExtractor.extractPreview(getContext(), message); + String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview(); assertEquals("text", preview); } @@ -104,7 +104,7 @@ public class MessagePreviewExtractorTest { MimeBodyPart htmlPart = new MimeBodyPart(htmlBody, "text/html"); multipart.addBodyPart(htmlPart); - String preview = MessagePreviewExtractor.extractPreview(getContext(), message); + String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview(); assertEquals("text / html", preview); } @@ -130,7 +130,7 @@ public class MessagePreviewExtractorTest { MimeBodyPart messagePart = new MimeBodyPart(innerMessage, "message/rfc822"); multipart.addBodyPart(messagePart); - String preview = MessagePreviewExtractor.extractPreview(getContext(), message); + String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview(); assertEquals("text / Includes message titled \"inner message\" containing: html", preview); } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index df0f899bd..0ed6db404 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -1247,10 +1247,10 @@ public class LocalFolder extends Folder implements Serializable { parentId = threadInfo.parentId; } - //TODO: get attachment count - try { - String preview = MessagePreviewExtractor.extractPreview(localStore.context, message); + MessageInfoExtractor messageExtractor = new MessageInfoExtractor(localStore.context, message); + String preview = messageExtractor.getMessageTextPreview(); + int attachmentCount = messageExtractor.getAttachmentCount(); long rootMessagePartId = saveMessageParts(db, message); @@ -1273,7 +1273,7 @@ public class LocalFolder extends Folder implements Serializable { cv.put("bcc_list", Address.pack(message.getRecipients(RecipientType.BCC))); cv.put("preview", preview); cv.put("reply_to_list", Address.pack(message.getReplyTo())); - cv.put("attachment_count", 0); //FIXME + cv.put("attachment_count", attachmentCount); cv.put("internal_date", message.getInternalDate() == null ? System.currentTimeMillis() : message.getInternalDate().getTime()); cv.put("mime_type", message.getMimeType()); diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/MessageInfoExtractor.java b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageInfoExtractor.java new file mode 100644 index 000000000..c0e5f66dc --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageInfoExtractor.java @@ -0,0 +1,43 @@ +package com.fsck.k9.mailstore; + + +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; + +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.internet.MessageExtractor; +import com.fsck.k9.mail.internet.Viewable; + + +class MessageInfoExtractor { + private final Context context; + private final Message message; + private List viewables; + private List attachments; + + public MessageInfoExtractor(Context context, Message message) { + this.context = context; + this.message = message; + } + + public String getMessageTextPreview() throws MessagingException { + getViewablesIfNecessary(); + return MessagePreviewExtractor.extractPreview(context, viewables); + } + + public int getAttachmentCount() throws MessagingException { + getViewablesIfNecessary(); + return attachments.size(); + } + + private void getViewablesIfNecessary() throws MessagingException { + if (viewables == null) { + attachments = new ArrayList(); + viewables = MessageExtractor.getViewables(message, attachments); + } + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/MessagePreviewExtractor.java b/k9mail/src/main/java/com/fsck/k9/mailstore/MessagePreviewExtractor.java index 60be77618..bafa7c82f 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/MessagePreviewExtractor.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/MessagePreviewExtractor.java @@ -1,7 +1,6 @@ package com.fsck.k9.mailstore; -import java.util.ArrayList; import java.util.List; import android.content.Context; @@ -9,8 +8,6 @@ import android.text.TextUtils; import com.fsck.k9.R; import com.fsck.k9.helper.HtmlConverter; -import com.fsck.k9.mail.Message; -import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MessageExtractor; import com.fsck.k9.mail.internet.Viewable; @@ -20,22 +17,11 @@ import com.fsck.k9.mail.internet.Viewable.MessageHeader; import com.fsck.k9.mail.internet.Viewable.Textual; -public class MessagePreviewExtractor { +class MessagePreviewExtractor { private static final int MAX_PREVIEW_LENGTH = 512; private static final int MAX_CHARACTERS_CHECKED_FOR_PREVIEW = 8192; - public static String extractPreview(Context context, Message message) throws MessagingException { - try { - List attachments = new ArrayList(); - List viewables = MessageExtractor.getViewables(message, attachments); - - return buildPreview(context, viewables); - } catch (Exception e) { - throw new MessagingException("Couldn't extract viewable parts", e); - } - } - - private static String buildPreview(Context context, List viewables) throws MessagingException { + public static String extractPreview(Context context, List viewables) { StringBuilder text = new StringBuilder(); boolean divider = false; From 74d09943c0fcf15e7f8e196462da63c154f3105d Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 22 Jan 2015 11:59:06 +0100 Subject: [PATCH 039/152] Use MimeMessageHelper.setBody() when parsing BODYSTRUCTURE This will correctly set the MIME type of the part containing the body. Otherwise multiparts end up having a content type of text/plain (default) in the database... oops. --- .../src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java b/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java index 6cf6825fe..65661738b 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java @@ -1710,7 +1710,7 @@ public class ImapStore extends RemoteStore { break; } } - part.setBody(mp); + MimeMessageHelper.setBody(part, mp); } else { /* * This is a body. We need to add as much information as we can find out about From 98bdf5467230e1d73f90fcbbb1052da3fccf96f8 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 23 Jan 2015 02:26:24 +0100 Subject: [PATCH 040/152] Don't save empty multipart body This will correctly mark the body as missing when the message is written to the database. --- .../main/java/com/fsck/k9/mail/Message.java | 2 +- .../src/main/java/com/fsck/k9/mail/Part.java | 2 +- .../fsck/k9/mail/internet/MimeBodyPart.java | 2 +- .../fsck/k9/mail/internet/MimeMessage.java | 30 +++++++++++++------ 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java index 46ff8302c..3f6e5515e 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java @@ -141,7 +141,7 @@ public abstract class Message implements Part, CompositeBody { public abstract void removeHeader(String name) throws MessagingException; @Override - public abstract void setBody(Body body) throws MessagingException; + public abstract void setBody(Body body); public abstract long getId(); diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java index 492b2a171..6f0de20cd 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java @@ -27,7 +27,7 @@ public interface Part { String getMimeType() throws MessagingException; - void setBody(Body body) throws MessagingException; + void setBody(Body body); void writeTo(OutputStream out) throws IOException, MessagingException; diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java index 801f1cbde..a39c9813b 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java @@ -72,7 +72,7 @@ public class MimeBodyPart extends BodyPart { } @Override - public void setBody(Body body) throws MessagingException { + public void setBody(Body body) { this.mBody = body; } diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java index 4cf09460e..120756dba 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java @@ -394,7 +394,7 @@ public class MimeMessage extends Message { } @Override - public void setBody(Body body) throws MessagingException { + public void setBody(Body body) { this.mBody = body; } @@ -492,13 +492,11 @@ public class MimeMessage extends Message { stack.addFirst(MimeMessage.this); } else { expect(Part.class); - try { - MimeMessage m = new MimeMessage(); - ((Part)stack.peek()).setBody(m); - stack.addFirst(m); - } catch (MessagingException me) { - throw new Error(me); - } + Part part = (Part) stack.peek(); + + MimeMessage m = new MimeMessage(); + part.setBody(m); + stack.addFirst(m); } } @@ -548,7 +546,21 @@ public class MimeMessage extends Message { @Override public void endMultipart() { - stack.removeFirst(); + expect(Multipart.class); + Multipart multipart = (Multipart) stack.removeFirst(); + + boolean hasNoBodyParts = multipart.getCount() == 0; + boolean hasNoEpilogue = multipart.getEpilogue() == null; + if (hasNoBodyParts && hasNoEpilogue) { + /* + * The parser is calling startMultipart(), preamble(), and endMultipart() when all we have is + * headers of a "multipart/*" part. But there's really no point in keeping a Multipart body if all + * of the content is missing. + */ + expect(Part.class); + Part part = (Part) stack.peek(); + part.setBody(null); + } } @Override From 564e2432e1845c11495c8f7876f81f7284a92548 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 23 Jan 2015 03:42:00 +0100 Subject: [PATCH 041/152] Get size of decoded body content when saving Before downloading we show the encoded size of attachments. After download we strip the transport encoding to find out the size of the decoded content. --- .../com/fsck/k9/mailstore/LocalFolder.java | 31 +++++++++++++++++-- .../com/fsck/k9/mailstore/LocalStore.java | 2 +- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 0ed6db404..53cc24db0 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -42,11 +42,13 @@ import com.fsck.k9.mail.MessageRetrievalListener; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.filter.CountingOutputStream; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.WrappedException; +import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.parser.ContentHandler; import org.apache.james.mime4j.parser.MimeStreamParser; @@ -1373,11 +1375,11 @@ public class LocalFolder extends Folder implements Serializable { byte[] bodyData = getBodyBytes(body); String encoding = getTransferEncoding(part); - if (attachment.size == AttachmentViewInfo.UNKNOWN_SIZE) { - //FIXME: Use size of content when transfer encoding is stripped + long size = decodeAndCountBytes(bodyData, encoding); + if (size == AttachmentViewInfo.UNKNOWN_SIZE) { cv.put("decoded_body_size", bodyData.length); } else { - cv.put("decoded_body_size", attachment.size); + cv.put("decoded_body_size", size); } cv.put("encoding", encoding); @@ -1387,6 +1389,29 @@ public class LocalFolder extends Folder implements Serializable { } } + private long decodeAndCountBytes(byte[] bodyData, String encoding) { + ByteArrayInputStream rawInputStream = new ByteArrayInputStream(bodyData); + return decodeAndCountBytes(encoding, rawInputStream); + } + + private long decodeAndCountBytes(String encoding, ByteArrayInputStream rawInputStream) { + InputStream decodingInputStream = localStore.getDecodingInputStream(rawInputStream, encoding); + try { + CountingOutputStream countingOutputStream = new CountingOutputStream(); + try { + IOUtils.copy(decodingInputStream, countingOutputStream); + + return countingOutputStream.getCount(); + } catch (IOException e) { + return AttachmentViewInfo.UNKNOWN_SIZE; + } + } finally { + try { + decodingInputStream.close(); + } catch (IOException e) { /* ignore */ } + } + } + private byte[] getHeaderBytes(Part part) throws IOException, MessagingException { ByteArrayOutputStream output = new ByteArrayOutputStream(); part.writeHeaderTo(output); diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java index a33b44bc0..e50015d91 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -717,7 +717,7 @@ public class LocalStore extends Store implements Serializable { } } - private InputStream getDecodingInputStream(InputStream rawInputStream, String encoding) { + InputStream getDecodingInputStream(InputStream rawInputStream, String encoding) { if (MimeUtil.ENC_BASE64.equals(encoding)) { return new Base64InputStream(rawInputStream); } From 6825eafb872ea79585785d28974cb551dae3613f Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 23 Jan 2015 14:41:29 +0100 Subject: [PATCH 042/152] Make column 'message_parts.data' a BLOB --- .../main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java b/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java index e57930a65..c8c2b187d 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java @@ -108,7 +108,7 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition { "encoding TEXT, " + "charset TEXT, " + "data_location INTEGER NOT NULL, " + - "data TEXT, " + + "data BLOB, " + "preamble TEXT, " + "epilogue TEXT, " + "boundary TEXT, " + From 977d15c1909fa476e157b0f1d55c8d200e9cb2e5 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 23 Jan 2015 14:56:52 +0100 Subject: [PATCH 043/152] Refactor to improve readability --- .../com/fsck/k9/mailstore/LocalFolder.java | 76 ++++++++++--------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 53cc24db0..c2033ad27 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -1354,47 +1354,51 @@ public class LocalFolder extends Folder implements Serializable { Body body = part.getBody(); if (body instanceof Multipart) { - cv.put("data_location", DataLocation.IN_DATABASE); - - Multipart multipart = (Multipart) body; - cv.put("preamble", multipart.getPreamble()); - cv.put("epilogue", multipart.getEpilogue()); - cv.put("boundary", multipart.getBoundary()); + multipartToContentValues(cv, (Multipart) body); + } else if (body == null) { + missingPartToContentValues(cv, part); } else { - AttachmentViewInfo attachment = LocalMessageExtractor.extractAttachmentInfo(part, PLACEHOLDER_URI); - - cv.put("display_name", attachment.displayName); - - if (body == null) { - //TODO: deal with missing parts - cv.put("data_location", DataLocation.MISSING); - cv.put("decoded_body_size", attachment.size); - } else { - cv.put("data_location", DataLocation.IN_DATABASE); - - byte[] bodyData = getBodyBytes(body); - String encoding = getTransferEncoding(part); - - long size = decodeAndCountBytes(bodyData, encoding); - if (size == AttachmentViewInfo.UNKNOWN_SIZE) { - cv.put("decoded_body_size", bodyData.length); - } else { - cv.put("decoded_body_size", size); - } - - cv.put("encoding", encoding); - cv.put("data", bodyData); - cv.put("content_id", part.getContentId()); - } + leafPartToContentValues(cv, part, body); } } - private long decodeAndCountBytes(byte[] bodyData, String encoding) { - ByteArrayInputStream rawInputStream = new ByteArrayInputStream(bodyData); - return decodeAndCountBytes(encoding, rawInputStream); + private void multipartToContentValues(ContentValues cv, Multipart multipart) { + cv.put("data_location", DataLocation.IN_DATABASE); + cv.put("preamble", multipart.getPreamble()); + cv.put("epilogue", multipart.getEpilogue()); + cv.put("boundary", multipart.getBoundary()); } - private long decodeAndCountBytes(String encoding, ByteArrayInputStream rawInputStream) { + private void missingPartToContentValues(ContentValues cv, Part part) throws MessagingException { + AttachmentViewInfo attachment = LocalMessageExtractor.extractAttachmentInfo(part, PLACEHOLDER_URI); + cv.put("display_name", attachment.displayName); + cv.put("data_location", DataLocation.MISSING); + cv.put("decoded_body_size", attachment.size); + } + + private void leafPartToContentValues(ContentValues cv, Part part, Body body) + throws MessagingException, IOException { + AttachmentViewInfo attachment = LocalMessageExtractor.extractAttachmentInfo(part, PLACEHOLDER_URI); + cv.put("display_name", attachment.displayName); + cv.put("data_location", DataLocation.IN_DATABASE); + + byte[] bodyData = getBodyBytes(body); + String encoding = getTransferEncoding(part); + + long size = decodeAndCountBytes(bodyData, encoding, bodyData.length); + cv.put("decoded_body_size", size); + + cv.put("encoding", encoding); + cv.put("data", bodyData); + cv.put("content_id", part.getContentId()); + } + + private long decodeAndCountBytes(byte[] bodyData, String encoding, long fallbackValue) { + ByteArrayInputStream rawInputStream = new ByteArrayInputStream(bodyData); + return decodeAndCountBytes(rawInputStream, encoding, fallbackValue); + } + + private long decodeAndCountBytes(ByteArrayInputStream rawInputStream, String encoding, long fallbackValue) { InputStream decodingInputStream = localStore.getDecodingInputStream(rawInputStream, encoding); try { CountingOutputStream countingOutputStream = new CountingOutputStream(); @@ -1403,7 +1407,7 @@ public class LocalFolder extends Folder implements Serializable { return countingOutputStream.getCount(); } catch (IOException e) { - return AttachmentViewInfo.UNKNOWN_SIZE; + return fallbackValue; } } finally { try { From 378acbd313ace01f89ea460a04c957c2c5267e60 Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 24 Jan 2015 03:06:45 +0100 Subject: [PATCH 044/152] Write large message parts to file system Actually, we just move the temporary file to avoid having to copy the data to a new file. --- .../k9/mail/internet/BinaryTempFileBody.java | 16 ++- .../com/fsck/k9/mail/internet/SizeAware.java | 6 + .../com/fsck/k9/mail/internet/TextBody.java | 32 ++++- .../fsck/k9/mailstore/BinaryMemoryBody.java | 8 +- .../com/fsck/k9/mailstore/LocalFolder.java | 120 ++++++++++++++---- .../com/fsck/k9/mailstore/LocalStore.java | 2 +- .../com/fsck/k9/mailstore/TempFileBody.java | 9 +- 7 files changed, 165 insertions(+), 28 deletions(-) create mode 100644 k9mail-library/src/main/java/com/fsck/k9/mail/internet/SizeAware.java diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java index 17870c140..157b4046c 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java @@ -1,6 +1,5 @@ package com.fsck.k9.mail.internet; -import com.fsck.k9.mail.Body; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.filter.Base64OutputStream; import org.apache.commons.io.IOUtils; @@ -15,7 +14,7 @@ import java.io.*; * and writeTo one time. After writeTo is called, or the InputStream returned from * getInputStream is closed the file is deleted and the Body should be considered disposed of. */ -public class BinaryTempFileBody implements RawDataBody { +public class BinaryTempFileBody implements RawDataBody, SizeAware { private static File mTempDirectory; private File mFile; @@ -26,6 +25,10 @@ public class BinaryTempFileBody implements RawDataBody { mTempDirectory = tempDirectory; } + public static File getTempDirectory() { + return mTempDirectory; + } + @Override public String getEncoding() { return mEncoding; @@ -101,6 +104,15 @@ public class BinaryTempFileBody implements RawDataBody { } } + @Override + public long getSize() { + return mFile.length(); + } + + public File getFile() { + return mFile; + } + class BinaryTempFileBodyInputStream extends FilterInputStream { public BinaryTempFileBodyInputStream(InputStream in) { super(in); diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/SizeAware.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/SizeAware.java new file mode 100644 index 000000000..07d5fdef9 --- /dev/null +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/SizeAware.java @@ -0,0 +1,6 @@ +package com.fsck.k9.mail.internet; + + +public interface SizeAware { + long getSize(); +} diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java index d6355662f..84162bd41 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java @@ -10,10 +10,11 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; +import com.fsck.k9.mail.filter.CountingOutputStream; import org.apache.james.mime4j.codec.QuotedPrintableOutputStream; import org.apache.james.mime4j.util.MimeUtil; -public class TextBody implements Body { +public class TextBody implements Body, SizeAware { /** * Immutable empty byte array @@ -98,4 +99,33 @@ public class TextBody implements Body { public void setComposedMessageOffset(Integer composedMessageOffset) { this.mComposedMessageOffset = composedMessageOffset; } + + @Override + public long getSize() { + try { + byte[] bytes = mBody.getBytes(mCharset); + + if (MimeUtil.ENC_8BIT.equalsIgnoreCase(mEncoding)) { + return bytes.length; + } else { + return getLengthWhenQuotedPrintableEncoded(bytes); + } + } catch (IOException e) { + throw new RuntimeException("Couldn't get body size", e); + } + } + + private long getLengthWhenQuotedPrintableEncoded(byte[] bytes) throws IOException { + CountingOutputStream countingOutputStream = new CountingOutputStream(); + OutputStream quotedPrintableOutputStream = new QuotedPrintableOutputStream(countingOutputStream, false); + try { + quotedPrintableOutputStream.write(bytes); + } finally { + try { + quotedPrintableOutputStream.close(); + } catch (IOException e) { /* ignore */ } + } + + return countingOutputStream.getCount(); + } } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java b/k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java index 46b9e8339..b4a289f3a 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java @@ -9,9 +9,10 @@ import java.io.OutputStream; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.RawDataBody; +import com.fsck.k9.mail.internet.SizeAware; -public class BinaryMemoryBody implements Body, RawDataBody { +public class BinaryMemoryBody implements Body, RawDataBody, SizeAware { private final byte[] data; private final String encoding; @@ -39,4 +40,9 @@ public class BinaryMemoryBody implements Body, RawDataBody { public void writeTo(OutputStream out) throws IOException, MessagingException { out.write(data); } + + @Override + public long getSize() { + return data.length; + } } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index c2033ad27..9b7905b85 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -4,8 +4,11 @@ package com.fsck.k9.mailstore; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; @@ -43,9 +46,11 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.filter.CountingOutputStream; +import com.fsck.k9.mail.internet.BinaryTempFileBody; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.SizeAware; import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.WrappedException; import org.apache.commons.io.IOUtils; @@ -62,6 +67,8 @@ public class LocalFolder extends Folder implements Serializable { private static final long serialVersionUID = -1973296520918624767L; private static final Uri PLACEHOLDER_URI = Uri.EMPTY; + private static final int MAX_BODY_SIZE_FOR_DATABASE = 16 * 1024; + private static final long INVALID_MESSAGE_PART_ID = -1; private final LocalStore localStore; @@ -1340,26 +1347,48 @@ public class LocalFolder extends Folder implements Serializable { cv.put("seq", order); cv.put("server_extra", part.getServerExtra()); - partToContentValues(cv, part); - - return db.insertOrThrow("message_parts", null, cv); + return updateOrInsertMessagePart(db, cv, part, INVALID_MESSAGE_PART_ID); } - private void partToContentValues(ContentValues cv, Part part) throws IOException, MessagingException { + private void renameTemporaryFile(File file, String messagePartId) { + File destination = localStore.getAttachmentFile(messagePartId); + if (!file.renameTo(destination)) { + Log.w(K9.LOG_TAG, "Couldn't rename temporary file " + file.getAbsolutePath() + + " to " + destination.getAbsolutePath()); + } + } + + private long updateOrInsertMessagePart(SQLiteDatabase db, ContentValues cv, Part part, long existingMessagePartId) + throws IOException, MessagingException { byte[] headerBytes = getHeaderBytes(part); cv.put("mime_type", part.getMimeType()); cv.put("header", headerBytes); cv.put("type", MessagePartType.UNKNOWN); + File file = null; Body body = part.getBody(); if (body instanceof Multipart) { multipartToContentValues(cv, (Multipart) body); } else if (body == null) { missingPartToContentValues(cv, part); } else { - leafPartToContentValues(cv, part, body); + file = leafPartToContentValues(cv, part, body); } + + long messagePartId; + if (existingMessagePartId != INVALID_MESSAGE_PART_ID) { + messagePartId = existingMessagePartId; + db.update("message_parts", cv, "id = ?", new String[] { Long.toString(messagePartId) }); + } else { + messagePartId = db.insertOrThrow("message_parts", null, cv); + } + + if (file != null) { + renameTemporaryFile(file, Long.toString(messagePartId)); + } + + return messagePartId; } private void multipartToContentValues(ContentValues cv, Multipart multipart) { @@ -1376,21 +1405,64 @@ public class LocalFolder extends Folder implements Serializable { cv.put("decoded_body_size", attachment.size); } - private void leafPartToContentValues(ContentValues cv, Part part, Body body) + private File leafPartToContentValues(ContentValues cv, Part part, Body body) throws MessagingException, IOException { AttachmentViewInfo attachment = LocalMessageExtractor.extractAttachmentInfo(part, PLACEHOLDER_URI); cv.put("display_name", attachment.displayName); - cv.put("data_location", DataLocation.IN_DATABASE); - byte[] bodyData = getBodyBytes(body); String encoding = getTransferEncoding(part); - long size = decodeAndCountBytes(bodyData, encoding, bodyData.length); - cv.put("decoded_body_size", size); + if (!(body instanceof SizeAware)) { + throw new IllegalStateException("Body needs to implement SizeAware"); + } + SizeAware sizeAwareBody = (SizeAware) body; + long fileSize = sizeAwareBody.getSize(); + + File file = null; + int dataLocation; + if (fileSize > MAX_BODY_SIZE_FOR_DATABASE) { + dataLocation = DataLocation.ON_DISK; + + file = writeBodyToDiskIfNecessary(part); + + long size = decodeAndCountBytes(file, encoding, fileSize); + cv.put("decoded_body_size", size); + } else { + dataLocation = DataLocation.IN_DATABASE; + + byte[] bodyData = getBodyBytes(body); + cv.put("data", bodyData); + + long size = decodeAndCountBytes(bodyData, encoding, bodyData.length); + cv.put("decoded_body_size", size); + } + cv.put("data_location", dataLocation); cv.put("encoding", encoding); - cv.put("data", bodyData); cv.put("content_id", part.getContentId()); + + return file; + } + + private File writeBodyToDiskIfNecessary(Part part) throws MessagingException, IOException { + Body body = part.getBody(); + if (body instanceof BinaryTempFileBody) { + return ((BinaryTempFileBody) body).getFile(); + } else { + return writeBodyToDisk(body); + } + } + + private File writeBodyToDisk(Body body) throws IOException, MessagingException { + File file = File.createTempFile("body", null, BinaryTempFileBody.getTempDirectory()); + OutputStream out = new FileOutputStream(file); + try { + body.writeTo(out); + } finally { + out.close(); + } + + return file; } private long decodeAndCountBytes(byte[] bodyData, String encoding, long fallbackValue) { @@ -1398,7 +1470,17 @@ public class LocalFolder extends Folder implements Serializable { return decodeAndCountBytes(rawInputStream, encoding, fallbackValue); } - private long decodeAndCountBytes(ByteArrayInputStream rawInputStream, String encoding, long fallbackValue) { + private long decodeAndCountBytes(File file, String encoding, long fallbackValue) + throws MessagingException, IOException { + InputStream inputStream = new FileInputStream(file); + try { + return decodeAndCountBytes(inputStream, encoding, fallbackValue); + } finally { + inputStream.close(); + } + } + + private long decodeAndCountBytes(InputStream rawInputStream, String encoding, long fallbackValue) { InputStream decodingInputStream = localStore.getDecodingInputStream(rawInputStream, encoding); try { CountingOutputStream countingOutputStream = new CountingOutputStream(); @@ -1464,7 +1546,7 @@ public class LocalFolder extends Folder implements Serializable { localStore.database.execute(false, new DbCallback() { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - String messagePartId; + long messagePartId; Cursor cursor = db.query("message_parts", new String[] { "id" }, "root = ? AND server_extra = ?", new String[] { Long.toString(message.getMessagePartId()), part.getServerExtra() }, @@ -1474,16 +1556,13 @@ public class LocalFolder extends Folder implements Serializable { throw new IllegalStateException("Message part not found"); } - messagePartId = cursor.getString(0); + messagePartId = cursor.getLong(0); } finally { cursor.close(); } try { - ContentValues cv = new ContentValues(); - partToContentValues(cv, part); - - db.update("message_parts", cv, "id = ?", new String[] { messagePartId }); + updateOrInsertMessagePart(db, new ContentValues(), part, messagePartId); } catch (Exception e) { Log.e(K9.LOG_TAG, "Error writing message part", e); } @@ -1686,16 +1765,13 @@ public class LocalFolder extends Folder implements Serializable { } private void deleteMessagePartsFromDisk(SQLiteDatabase db, long rootMessagePartId) { - File attachmentDirectory = StorageManager.getInstance(localStore.context) - .getAttachmentDirectory(getAccountUuid(), localStore.database.getStorageProviderId()); - Cursor cursor = db.query("message_parts", new String[] { "id" }, "root = ? AND data_location = " + DataLocation.ON_DISK, new String[] { Long.toString(rootMessagePartId) }, null, null, null); try { while (cursor.moveToNext()) { String messagePartId = cursor.getString(0); - File file = new File(attachmentDirectory, messagePartId); + File file = localStore.getAttachmentFile(messagePartId); if (file.exists()) { if (!file.delete() && K9.DEBUG) { Log.d(K9.LOG_TAG, "Couldn't delete message part file: " + file.getAbsolutePath()); diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java index e50015d91..e19da4877 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -728,7 +728,7 @@ public class LocalStore extends Store implements Serializable { return rawInputStream; } - private File getAttachmentFile(String attachmentId) { + File getAttachmentFile(String attachmentId) { final StorageManager storageManager = StorageManager.getInstance(context); final File attachmentDirectory = storageManager.getAttachmentDirectory(uUid, database.getStorageProviderId()); return new File(attachmentDirectory, attachmentId); diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/TempFileBody.java b/k9mail/src/main/java/com/fsck/k9/mailstore/TempFileBody.java index 934b3bd68..5dea69d53 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/TempFileBody.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/TempFileBody.java @@ -7,11 +7,13 @@ import java.io.FileNotFoundException; import java.io.InputStream; import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.internet.SizeAware; + /** * An attachment whose contents are contained in a file. */ -public class TempFileBody extends BinaryAttachmentBody { +public class TempFileBody extends BinaryAttachmentBody implements SizeAware { private final File mFile; public TempFileBody(String filename) { @@ -26,4 +28,9 @@ public class TempFileBody extends BinaryAttachmentBody { return new ByteArrayInputStream(LocalStore.EMPTY_BYTE_ARRAY); } } + + @Override + public long getSize() { + return mFile.length(); + } } From 0e03f262b3bb8c1d09d56f6b16fd1e4309356dd0 Mon Sep 17 00:00:00 2001 From: cketti Date: Sun, 25 Jan 2015 20:06:29 +0100 Subject: [PATCH 045/152] Make sure to close underlying InputStream after decoding attachments --- .../com/fsck/k9/mailstore/LocalStore.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java index e19da4877..7236ec786 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -39,6 +39,7 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.util.ArrayList; @@ -717,12 +718,24 @@ public class LocalStore extends Store implements Serializable { } } - InputStream getDecodingInputStream(InputStream rawInputStream, String encoding) { + InputStream getDecodingInputStream(final InputStream rawInputStream, String encoding) { if (MimeUtil.ENC_BASE64.equals(encoding)) { - return new Base64InputStream(rawInputStream); + return new Base64InputStream(rawInputStream) { + @Override + public void close() throws IOException { + super.close(); + rawInputStream.close(); + } + }; } if (MimeUtil.ENC_QUOTED_PRINTABLE.equals(encoding)) { - return new QuotedPrintableInputStream(rawInputStream); + return new QuotedPrintableInputStream(rawInputStream) { + @Override + public void close() throws IOException { + super.close(); + rawInputStream.close(); + } + }; } return rawInputStream; From 8f7f65635559fa3e2e1aae62ad3a6790225cfe81 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 26 Jan 2015 20:25:43 +0100 Subject: [PATCH 046/152] Add method to find multipart/encrypted parts --- .../main/java/com/fsck/k9/mail/Message.java | 3 - .../src/main/java/com/fsck/k9/mail/Part.java | 4 +- .../fsck/k9/mail/internet/MimeBodyPart.java | 4 +- .../fsck/k9/mail/internet/MimeMessage.java | 6 +- .../fsck/k9/crypto/MessageDecryptorTest.java | 78 +++++++++++++++++++ .../com/fsck/k9/crypto/MessageDecryptor.java | 41 ++++++++++ 6 files changed, 127 insertions(+), 9 deletions(-) create mode 100644 k9mail/src/androidTest/java/com/fsck/k9/crypto/MessageDecryptorTest.java create mode 100644 k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptor.java diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java index 3f6e5515e..67ba3a244 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java @@ -120,9 +120,6 @@ public abstract class Message implements Part, CompositeBody { @Override public abstract Body getBody(); - @Override - public abstract String getContentType() throws MessagingException; - @Override public abstract void addHeader(String name, String value) throws MessagingException; diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java index 6f0de20cd..190ae378b 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java @@ -15,7 +15,7 @@ public interface Part { Body getBody(); - String getContentType() throws MessagingException; + String getContentType(); String getDisposition() throws MessagingException; @@ -25,7 +25,7 @@ public interface Part { boolean isMimeType(String mimeType) throws MessagingException; - String getMimeType() throws MessagingException; + String getMimeType(); void setBody(Body body); diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java index a39c9813b..3c0103541 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java @@ -85,7 +85,7 @@ public class MimeBodyPart extends BodyPart { } @Override - public String getContentType() throws MessagingException { + public String getContentType() { String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); return (contentType == null) ? "text/plain" : contentType; } @@ -111,7 +111,7 @@ public class MimeBodyPart extends BodyPart { } @Override - public String getMimeType() throws MessagingException { + public String getMimeType() { return MimeUtility.getHeaderParameter(getContentType(), null); } diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java index 120756dba..91ab8c812 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java @@ -163,7 +163,7 @@ public class MimeMessage extends Message { } @Override - public String getContentType() throws MessagingException { + public String getContentType() { String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); return (contentType == null) ? "text/plain" : contentType; } @@ -172,12 +172,14 @@ public class MimeMessage extends Message { public String getDisposition() throws MessagingException { return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION); } + @Override public String getContentId() throws MessagingException { return null; } + @Override - public String getMimeType() throws MessagingException { + public String getMimeType() { return MimeUtility.getHeaderParameter(getContentType(), null); } diff --git a/k9mail/src/androidTest/java/com/fsck/k9/crypto/MessageDecryptorTest.java b/k9mail/src/androidTest/java/com/fsck/k9/crypto/MessageDecryptorTest.java new file mode 100644 index 000000000..17782d318 --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/crypto/MessageDecryptorTest.java @@ -0,0 +1,78 @@ +package com.fsck.k9.crypto; + + +import java.util.List; + +import android.support.test.runner.AndroidJUnit4; + +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.internet.MimeBodyPart; +import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.internet.MimeMessageHelper; +import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.TextBody; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertSame; + + +@RunWith(AndroidJUnit4.class) +public class MessageDecryptorTest { + + @Test + public void findEncryptedPartsShouldReturnEmptyListForEmptyMessage() throws Exception { + MimeMessage emptyMessage = new MimeMessage(); + + List encryptedParts = MessageDecryptor.findEncryptedParts(emptyMessage); + assertEquals(0, encryptedParts.size()); + } + + @Test + public void findEncryptedPartsShouldReturnEmptyListForSimpleMessage() throws Exception { + MimeMessage message = new MimeMessage(); + message.setBody(new TextBody("message text")); + + List encryptedParts = MessageDecryptor.findEncryptedParts(message); + assertEquals(0, encryptedParts.size()); + } + + @Test + public void findEncryptedPartsShouldReturnEmptyEncryptedPart() throws Exception { + MimeMessage message = new MimeMessage(); + MimeMultipart mulitpartEncrypted = new MimeMultipart(); + mulitpartEncrypted.setSubType("encrypted"); + MimeMessageHelper.setBody(message, mulitpartEncrypted); + + List encryptedParts = MessageDecryptor.findEncryptedParts(message); + assertEquals(1, encryptedParts.size()); + assertSame(message, encryptedParts.get(0)); + } + + @Test + public void findEncryptedPartsShouldReturnMultipleEncryptedParts() throws Exception { + MimeMessage message = new MimeMessage(); + MimeMultipart multipartMixed = new MimeMultipart(); + multipartMixed.setSubType("mixed"); + MimeMessageHelper.setBody(message, multipartMixed); + + MimeMultipart mulitpartEncryptedOne = new MimeMultipart(); + mulitpartEncryptedOne.setSubType("encrypted"); + MimeBodyPart bodyPartOne = new MimeBodyPart(mulitpartEncryptedOne); + multipartMixed.addBodyPart(bodyPartOne); + + MimeBodyPart bodyPartTwo = new MimeBodyPart(null, "text/plain"); + multipartMixed.addBodyPart(bodyPartTwo); + + MimeMultipart mulitpartEncryptedThree = new MimeMultipart(); + mulitpartEncryptedThree.setSubType("encrypted"); + MimeBodyPart bodyPartThree = new MimeBodyPart(mulitpartEncryptedThree); + multipartMixed.addBodyPart(bodyPartThree); + + List encryptedParts = MessageDecryptor.findEncryptedParts(message); + assertEquals(2, encryptedParts.size()); + assertSame(bodyPartOne, encryptedParts.get(0)); + assertSame(bodyPartThree, encryptedParts.get(1)); + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptor.java b/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptor.java new file mode 100644 index 000000000..f15acad9d --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptor.java @@ -0,0 +1,41 @@ +package com.fsck.k9.crypto; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Multipart; +import com.fsck.k9.mail.Part; + + +public class MessageDecryptor { + private static final String MULTIPART_ENCRYPTED = "multipart/encrypted"; + + public static List findEncryptedParts(Part startPart) { + List encryptedParts = new ArrayList(); + Stack partsToCheck = new Stack(); + partsToCheck.push(startPart); + + while (!partsToCheck.isEmpty()) { + Part part = partsToCheck.pop(); + String mimeType = part.getMimeType(); + Body body = part.getBody(); + + if (MULTIPART_ENCRYPTED.equals(mimeType)) { + encryptedParts.add(part); + } else if (body instanceof Multipart) { + Multipart multipart = (Multipart) body; + for (int i = multipart.getCount() - 1; i >= 0; i--) { + BodyPart bodyPart = multipart.getBodyPart(i); + partsToCheck.push(bodyPart); + } + } + } + + return encryptedParts; + } +} From 40b62287561a4cc34cdc4dbf7dfa14abd162cdf9 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Tue, 27 Jan 2015 12:55:47 +0100 Subject: [PATCH 047/152] new MessageViewInfo structure (with transitional methods) --- .../fsck/k9/mailstore/MessageViewInfo.java | 44 ++++++++++++++++--- .../k9/ui/messageview/SingleMessageView.java | 6 +-- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java index 98f8fc2b2..6f768fb56 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java @@ -1,20 +1,54 @@ package com.fsck.k9.mailstore; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.fsck.k9.mail.Message; +import org.openintents.openpgp.OpenPgpSignatureResult; public class MessageViewInfo { - public final String text; - public final List attachments; - public final Message message; + public final Message message; + public final List containers = new ArrayList(); + + @Deprecated public MessageViewInfo(String text, List attachments, Message message) { - this.text = text; - this.attachments = Collections.unmodifiableList(attachments); + containers.add(new MessageViewContainer(text, attachments)); this.message = message; } + + @Deprecated + public String getText() { + return containers.get(0).text; + } + + @Deprecated + public List getAttachments() { + return containers.get(0).attachments; + } + + public static class MessageViewContainer { + + final public String text; + final public List attachments; + final public OpenPgpSignatureResult signatureResult; + + MessageViewContainer(String text, List attachments) { + this.text = text; + this.attachments = attachments; + this.signatureResult = null; + } + + MessageViewContainer(String text, List attachments, + OpenPgpSignatureResult signatureResult) { + this.text = text; + this.attachments = attachments; + this.signatureResult = signatureResult; + } + + } + } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java index ac9ed9953..678b9cd9b 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java @@ -533,13 +533,13 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, } if (text == null) { - text = messageViewInfo.text; + text = messageViewInfo.getText(); } // Save the text so we can reset the WebView when the user clicks the "Show pictures" button mText = text; - mHasAttachments = !messageViewInfo.attachments.isEmpty(); + mHasAttachments = !messageViewInfo.getAttachments().isEmpty(); if (mHasAttachments) { renderAttachments(messageViewInfo); } @@ -616,7 +616,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, } public void renderAttachments(MessageViewInfo messageContainer) throws MessagingException { - for (AttachmentViewInfo attachment : messageContainer.attachments) { + for (AttachmentViewInfo attachment : messageContainer.getAttachments()) { AttachmentView view = (AttachmentView) mInflater.inflate(R.layout.message_view_attachment, null); view.setCallback(attachmentCallback); view.setAttachment(attachment); From 29ad0f0f99efb03fc5c5a5202ac015ac7e67f2d2 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Tue, 27 Jan 2015 16:42:31 +0100 Subject: [PATCH 048/152] rename SingleMessageView to MessageContainerView --- .../{SingleMessageView.java => MessageContainerView.java} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename k9mail/src/main/java/com/fsck/k9/ui/messageview/{SingleMessageView.java => MessageContainerView.java} (99%) diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java similarity index 99% rename from k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java rename to k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java index 678b9cd9b..1a816884e 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/SingleMessageView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java @@ -63,7 +63,7 @@ import com.fsck.k9.view.MessageWebView; import org.apache.commons.io.IOUtils; -public class SingleMessageView extends LinearLayout implements OnClickListener, +public class MessageContainerView extends LinearLayout implements OnClickListener, OnLayoutChangedListener, OnCreateContextMenuListener { private static final int MENU_ITEM_LINK_VIEW = Menu.FIRST; private static final int MENU_ITEM_LINK_SHARE = Menu.FIRST + 1; @@ -419,7 +419,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener, showAttachments(true); } - public SingleMessageView(Context context, AttributeSet attrs) { + public MessageContainerView(Context context, AttributeSet attrs) { super(context, attrs); } From 445c978f31fdf59d306887cbae34804a4028a113 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Tue, 27 Jan 2015 16:43:49 +0100 Subject: [PATCH 049/152] extract header view for multiple MessageContainerViews (intermediate state) --- .../fsck/k9/mailstore/MessageViewInfo.java | 12 +- .../ui/messageview/MessageContainerView.java | 99 ++++------------- .../k9/ui/messageview/MessageTopView.java | 105 ++++++++++++++++++ .../ui/messageview/MessageViewFragment.java | 39 +++---- k9mail/src/main/res/layout/message.xml | 48 ++------ .../src/main/res/layout/message_container.xml | 43 +++++++ 6 files changed, 200 insertions(+), 146 deletions(-) create mode 100644 k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageTopView.java create mode 100644 k9mail/src/main/res/layout/message_container.xml diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java index 6f768fb56..cf86e63f0 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java @@ -16,20 +16,12 @@ public class MessageViewInfo { @Deprecated public MessageViewInfo(String text, List attachments, Message message) { + containers.add(new MessageViewContainer(text, attachments)); + // FIXME just display it twice, for testing only containers.add(new MessageViewContainer(text, attachments)); this.message = message; } - @Deprecated - public String getText() { - return containers.get(0).text; - } - - @Deprecated - public List getAttachments() { - return containers.get(0).attachments; - } - public static class MessageViewContainer { final public String text; diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java index 1a816884e..92ee79a3d 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java @@ -20,7 +20,6 @@ import android.os.AsyncTask; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; -import android.util.Log; import android.util.TypedValue; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; @@ -55,9 +54,9 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.AttachmentViewInfo; import com.fsck.k9.mailstore.MessageViewInfo; +import com.fsck.k9.mailstore.MessageViewInfo.MessageViewContainer; import com.fsck.k9.provider.AttachmentProvider.AttachmentProviderColumns; -import com.fsck.k9.view.MessageHeader; import com.fsck.k9.view.MessageHeader.OnLayoutChangedListener; import com.fsck.k9.view.MessageWebView; import org.apache.commons.io.IOUtils; @@ -90,7 +89,6 @@ public class MessageContainerView extends LinearLayout implements OnClickListene private MessageOpenPgpView mOpenPgpView; private MessageWebView mMessageContentView; - private MessageHeader mHeaderContainer; private LinearLayout mAttachments; private Button mShowHiddenAttachments; private LinearLayout mHiddenAttachments; @@ -117,18 +115,12 @@ public class MessageContainerView extends LinearLayout implements OnClickListene activity.registerForContextMenu(mMessageContentView); mMessageContentView.setOnCreateContextMenuListener(this); - mHeaderContainer = (MessageHeader) findViewById(R.id.header_container); - mHeaderContainer.setOnLayoutChangedListener(this); - mAttachmentsContainer = findViewById(R.id.attachments_container); mAttachments = (LinearLayout) findViewById(R.id.attachments); mHiddenAttachments = (LinearLayout) findViewById(R.id.hidden_attachments); mHiddenAttachments.setVisibility(View.GONE); mShowHiddenAttachments = (Button) findViewById(R.id.show_hidden_attachments); mShowHiddenAttachments.setVisibility(View.GONE); - mOpenPgpView = (MessageOpenPgpView) findViewById(R.id.layout_decrypt_openpgp); - mOpenPgpView.setFragment(fragment); - mOpenPgpView.setupChildViews(); mShowPicturesAction = findViewById(R.id.show_pictures); mShowMessageAction = findViewById(R.id.show_message); @@ -139,8 +131,8 @@ public class MessageContainerView extends LinearLayout implements OnClickListene mContacts = Contacts.getInstance(activity); mInflater = ((MessageViewFragment) fragment).getFragmentLayoutInflater(); - mDownloadRemainder = (Button) findViewById(R.id.download_remainder); - mDownloadRemainder.setVisibility(View.GONE); + // mDownloadRemainder = (Button) findViewById(R.id.download_remainder); + // mDownloadRemainder.setVisibility(View.GONE); mAttachmentsContainer.setVisibility(View.GONE); mMessageContentView.setVisibility(View.VISIBLE); @@ -148,14 +140,13 @@ public class MessageContainerView extends LinearLayout implements OnClickListene // titlebar, which is really unfair. TypedValue outValue = new TypedValue(); getContext().getTheme().resolveAttribute(R.attr.messageViewHeaderBackgroundColor, outValue, true); - mHeaderContainer.setBackgroundColor(outValue.data); // also set background of the whole view (including the attachments view) setBackgroundColor(outValue.data); mShowHiddenAttachments.setOnClickListener(this); - mShowMessageAction.setOnClickListener(this); - mShowAttachmentsAction.setOnClickListener(this); - mShowPicturesAction.setOnClickListener(this); + // mShowMessageAction.setOnClickListener(this); + // mShowAttachmentsAction.setOnClickListener(this); + // mShowPicturesAction.setOnClickListener(this); mClipboardManager = ClipboardManager.getInstance(activity); } @@ -446,37 +437,13 @@ public class MessageContainerView extends LinearLayout implements OnClickListene } public void showShowPicturesAction(boolean show) { - mShowPicturesAction.setVisibility(show ? View.VISIBLE : View.GONE); + // mShowPicturesAction.setVisibility(show ? View.VISIBLE : View.GONE); } public void showShowMessageAction(boolean show) { - mShowMessageAction.setVisibility(show ? View.VISIBLE : View.GONE); + // mShowMessageAction.setVisibility(show ? View.VISIBLE : View.GONE); } public void showShowAttachmentsAction(boolean show) { - mShowAttachmentsAction.setVisibility(show ? View.VISIBLE : View.GONE); - } - - /** - * Fetch the message header view. This is not the same as the message headers; this is the View shown at the top - * of messages. - * @return MessageHeader View. - */ - public MessageHeader getMessageHeaderView() { - return mHeaderContainer; - } - - public void setHeaders(final Message message, Account account) { - try { - mHeaderContainer.populate(message, account); - mHeaderContainer.setVisibility(View.VISIBLE); - - - } catch (Exception me) { - Log.e(K9.LOG_TAG, "setHeaders - error", me); - } - } - - public void setOnToggleFlagClickListener(OnClickListener listener) { - mHeaderContainer.setOnFlagListener(listener); + // mShowAttachmentsAction.setVisibility(show ? View.VISIBLE : View.GONE); } public void setOnDownloadButtonClickListener(OnClickListener listener) { @@ -512,36 +479,16 @@ public class MessageContainerView extends LinearLayout implements OnClickListene } } - public void showAllHeaders() { - mHeaderContainer.onShowAdditionalHeaders(); - } - - public boolean additionalHeadersVisible() { - return mHeaderContainer.additionalHeadersVisible(); - } - - public void setMessage(Account account, MessageViewInfo messageViewInfo, PgpData pgpData) + public void setMessage(MessageViewContainer messageViewContainer) throws MessagingException { resetView(); - String text = null; - if (pgpData != null) { - text = pgpData.getDecryptedData(); - if (text != null) { - text = HtmlConverter.textToHtml(text); - } - } - - if (text == null) { - text = messageViewInfo.getText(); - } - // Save the text so we can reset the WebView when the user clicks the "Show pictures" button - mText = text; + mText = messageViewContainer.text; - mHasAttachments = !messageViewInfo.getAttachments().isEmpty(); + mHasAttachments = !messageViewContainer.attachments.isEmpty(); if (mHasAttachments) { - renderAttachments(messageViewInfo); + renderAttachments(messageViewContainer); } mHiddenAttachments.setVisibility(View.GONE); @@ -568,12 +515,13 @@ public class MessageContainerView extends LinearLayout implements OnClickListene onShowMessage(); } + /* if (text != null && lookForImages) { // If the message contains external pictures and the "Show pictures" // button wasn't already pressed, see if the user's preferences has us // showing them anyway. if (Utility.hasExternalImages(text) && !showPictures()) { - Address[] from = messageViewInfo.message.getFrom(); + Address[] from = messageViewContainer.message.getFrom(); if ((account.getShowPictures() == Account.ShowPictures.ALWAYS) || ((account.getShowPictures() == Account.ShowPictures.ONLY_FROM_CONTACTS) && // Make sure we have at least one from address @@ -585,11 +533,10 @@ public class MessageContainerView extends LinearLayout implements OnClickListene } } } + */ - if (text != null) { - loadBodyFromText(text); - mOpenPgpView.updateLayout(account, pgpData.getDecryptedData(), - pgpData.getSignatureResult(), messageViewInfo.message); + if (mText != null) { + loadBodyFromText(mText); } else { showStatusMessage(getContext().getString(R.string.webview_empty_message)); } @@ -615,8 +562,8 @@ public class MessageContainerView extends LinearLayout implements OnClickListene mMessageContentView.setVisibility(show ? View.VISIBLE : View.GONE); } - public void renderAttachments(MessageViewInfo messageContainer) throws MessagingException { - for (AttachmentViewInfo attachment : messageContainer.getAttachments()) { + public void renderAttachments(MessageViewContainer messageContainer) throws MessagingException { + for (AttachmentViewInfo attachment : messageContainer.attachments) { AttachmentView view = (AttachmentView) mInflater.inflate(R.layout.message_view_attachment, null); view.setCallback(attachmentCallback); view.setAttachment(attachment); @@ -652,7 +599,7 @@ public class MessageContainerView extends LinearLayout implements OnClickListene } public void resetView() { - mDownloadRemainder.setVisibility(View.GONE); + // mDownloadRemainder.setVisibility(View.GONE); setLoadPictures(false); showShowAttachmentsAction(false); showShowMessageAction(false); @@ -670,10 +617,6 @@ public class MessageContainerView extends LinearLayout implements OnClickListene loadBodyFromText(""); } - public void resetHeaderView() { - mHeaderContainer.setVisibility(View.GONE); - } - public void setAttachmentCallback(AttachmentViewCallback attachmentCallback) { this.attachmentCallback = attachmentCallback; } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageTopView.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageTopView.java new file mode 100644 index 000000000..bf9f391bb --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageTopView.java @@ -0,0 +1,105 @@ +package com.fsck.k9.ui.messageview; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; + +import com.fsck.k9.Account; +import com.fsck.k9.K9; +import com.fsck.k9.R; +import com.fsck.k9.crypto.PgpData; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mailstore.MessageViewInfo; +import com.fsck.k9.mailstore.MessageViewInfo.MessageViewContainer; +import com.fsck.k9.view.MessageHeader; + + +public class MessageTopView extends LinearLayout { + + private MessageHeader mHeaderContainer; + private LayoutInflater mInflater; + private LinearLayout containerViews; + private Fragment fragment; + + public MessageTopView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void initialize (Fragment fragment) { + this.fragment = fragment; + Activity activity = fragment.getActivity(); + + mHeaderContainer = (MessageHeader) findViewById(R.id.header_container); + // mHeaderContainer.setOnLayoutChangedListener(this); + mInflater = ((MessageViewFragment) fragment).getFragmentLayoutInflater(); + + TypedValue outValue = new TypedValue(); + getContext().getTheme().resolveAttribute(R.attr.messageViewHeaderBackgroundColor, outValue, true); + mHeaderContainer.setBackgroundColor(outValue.data); + + containerViews = (LinearLayout) findViewById(R.id.message_containers); + + } + + public void resetView() { + // mDownloadRemainder.setVisibility(View.GONE); + containerViews.removeAllViews(); + } + + public void setMessage(Account account, MessageViewInfo messageViewInfo) + throws MessagingException { + resetView(); + + for (MessageViewContainer container : messageViewInfo.containers) { + MessageContainerView view = (MessageContainerView) mInflater.inflate(R.layout.message_container, null); + view.initialize(fragment); + view.setMessage(container); + containerViews.addView(view); + } + + } + + /** + * Fetch the message header view. This is not the same as the message headers; this is the View shown at the top + * of messages. + * @return MessageHeader View. + */ + public MessageHeader getMessageHeaderView() { + return mHeaderContainer; + } + + public void setHeaders(final Message message, Account account) { + try { + mHeaderContainer.populate(message, account); + mHeaderContainer.setVisibility(View.VISIBLE); + + + } catch (Exception me) { + Log.e(K9.LOG_TAG, "setHeaders - error", me); + } + } + + public void setOnToggleFlagClickListener(OnClickListener listener) { + mHeaderContainer.setOnFlagListener(listener); + } + + public void showAllHeaders() { + mHeaderContainer.onShowAdditionalHeaders(); + } + + public boolean additionalHeadersVisible() { + return mHeaderContainer.additionalHeadersVisible(); + } + + public void resetHeaderView() { + mHeaderContainer.setVisibility(View.GONE); + } + +} diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java index 61ff0c9be..95d36314b 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java @@ -78,7 +78,7 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF } - private SingleMessageView mMessageView; + private MessageTopView mMessageView; private PgpData mPgpData; private Account mAccount; private MessageReference mMessageReference; @@ -145,9 +145,9 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF View view = mLayoutInflater.inflate(R.layout.message, container, false); - mMessageView = (SingleMessageView) view.findViewById(R.id.message_view); + mMessageView = (MessageTopView) view.findViewById(R.id.message_view); - mMessageView.setAttachmentCallback(this); + // mMessageView.setAttachmentCallback(this); mMessageView.initialize(this); mMessageView.setOnToggleFlagClickListener(new OnClickListener() { @@ -156,12 +156,13 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF onToggleFlagged(); } }); - mMessageView.setOnDownloadButtonClickListener(new OnClickListener() { + + /*mMessageView.setOnDownloadButtonClickListener(new OnClickListener() { @Override public void onClick(View v) { onDownloadRemainder(); } - }); + });*/ mFragmentListener.messageHeaderViewAvailable(mMessageView.getMessageHeaderView()); @@ -229,7 +230,7 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF } private void onLoadMessageFromDatabaseFailed() { - mMessageView.showStatusMessage(mContext.getString(R.string.status_invalid_id_error)); + // mMessageView.showStatusMessage(mContext.getString(R.string.status_invalid_id_error)); } private void startDownloadingMessageBody(LocalMessage message) { @@ -247,7 +248,7 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF } private void onDownloadMessageFailed(Throwable t) { - mMessageView.enableDownloadButton(); + // mMessageView.enableDownloadButton(); String errorMessage; if (t instanceof IllegalArgumentException) { errorMessage = mContext.getString(R.string.status_invalid_id_error); @@ -269,8 +270,8 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF private void showMessage(MessageViewInfo messageContainer) { try { - mMessageView.setMessage(mAccount, messageContainer, mPgpData); - mMessageView.setShowDownloadButton(mMessage); + mMessageView.setMessage(mAccount, messageContainer); + // mMessageView.setShowDownloadButton(mMessage); } catch (MessagingException e) { Log.e(K9.LOG_TAG, "Error while trying to display message", e); } @@ -406,7 +407,8 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF } public void onSelectText() { - mMessageView.beginSelectingText(); + // FIXME + // mMessageView.beginSelectingText(); } private void startRefileActivity(int activity) { @@ -418,7 +420,6 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF startActivityForResult(intent, activity); } - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode != Activity.RESULT_OK) { @@ -487,7 +488,7 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF if (mMessage.isSet(Flag.X_DOWNLOADED_FULL)) { return; } - mMessageView.disableDownloadButton(); + // mMessageView.disableDownloadButton(); mController.loadMessageForViewRemote(mAccount, mMessageReference.folderName, mMessageReference.uid, downloadMessageListener); @@ -533,7 +534,7 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF PgpData data = new PgpData(); data.setDecryptedData(decryptedData); data.setSignatureResult(signatureResult); - mMessageView.setMessage(mAccount, messageViewInfo, data); + mMessageView.setMessage(mAccount, messageViewInfo); } catch (MessagingException e) { Log.e(K9.LOG_TAG, "displayMessageBody failed", e); } @@ -600,7 +601,7 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF } public void zoom(KeyEvent event) { - mMessageView.zoom(event); + // mMessageView.zoom(event); } @Override @@ -668,11 +669,11 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF } public void disableAttachmentButtons(AttachmentViewInfo attachment) { - mMessageView.disableAttachmentButtons(attachment); + // mMessageView.disableAttachmentButtons(attachment); } public void enableAttachmentButtons(AttachmentViewInfo attachment) { - mMessageView.enableAttachmentButtons(attachment); + // mMessageView.enableAttachmentButtons(attachment); } public void runOnMainThread(Runnable runnable) { @@ -680,7 +681,7 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF } public void showAttachmentLoadingDialog() { - mMessageView.disableAttachmentButtons(); + // mMessageView.disableAttachmentButtons(); showDialog(R.id.dialog_attachment_progress); } @@ -689,13 +690,13 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF @Override public void run() { removeDialog(R.id.dialog_attachment_progress); - mMessageView.enableAttachmentButtons(); + // mMessageView.enableAttachmentButtons(); } }); } public void refreshAttachmentThumbnail(AttachmentViewInfo attachment) { - mMessageView.refreshAttachmentThumbnail(attachment); + // mMessageView.refreshAttachmentThumbnail(attachment); } public interface MessageViewFragmentListener { diff --git a/k9mail/src/main/res/layout/message.xml b/k9mail/src/main/res/layout/message.xml index cbf2552de..a3f18a080 100644 --- a/k9mail/src/main/res/layout/message.xml +++ b/k9mail/src/main/res/layout/message.xml @@ -1,5 +1,5 @@ - - - - - - - - - - - -