From 0dff228dc769e141ec2a27d951963a0d705ddabb Mon Sep 17 00:00:00 2001 From: Mihai Preda Date: Wed, 10 Jun 2009 10:48:32 -0700 Subject: [PATCH 1/2] AI 149714: Download inline images for viewing if necessary and don't delete attachment cache files if these are inline images. The purpose of original logic of loadAttachmnet() is to keep at most one attachment cache, probably to limit the size of cached file. But it also purges all inline images. Integrates CL 149551 from DocomoEmail. BUG=1884385,1860250 Automated import of CL 149714 --- res/values/strings.xml | 2 + .../android/email/MessagingController.java | 79 +++++++++- src/com/android/email/MessagingListener.java | 12 ++ .../android/email/activity/MessageView.java | 142 ++++++++---------- .../email/mail/internet/EmailHtmlUtil.java | 88 ++++++++++- .../android/email/mail/store/LocalStore.java | 19 ++- .../mail/internet/EmailHtmlUtilTest.java | 2 + 7 files changed, 255 insertions(+), 89 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 5cc749069..478c52c65 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -195,6 +195,8 @@ Show pictures Fetching attachment. + + Fetching images\u2026 Fetching attachment %s diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java index e4ebada69..bc9aff822 100644 --- a/src/com/android/email/MessagingController.java +++ b/src/com/android/email/MessagingController.java @@ -1349,7 +1349,72 @@ public class MessagingController implements Runnable { }); } - public void loadMessageForView(final Account account, final String folder, final String uid, + private boolean isInlineImage(Part part) throws MessagingException { + String contentId = part.getContentId(); + String mimeType = part.getMimeType(); + return contentId != null && mimeType != null && mimeType.startsWith("image/"); + } + + public void loadInlineImagesForView(final Account account, final Message message, + MessagingListener listener) { + synchronized (mListeners) { + for (MessagingListener l : mListeners) { + l.loadInlineImagesForViewStarted(account, message); + } + } + try { + LocalStore localStore = (LocalStore)Store.getInstance(account.getLocalStoreUri(), + mContext, null); + LocalFolder localFolder = (LocalFolder)message.getFolder(); + localFolder.open(OpenMode.READ_WRITE, null); + + // Download inline images if necessary. + Folder remoteFolder = null; + ArrayList viewables = new ArrayList(); + ArrayList attachments = new ArrayList(); + MimeUtility.collectParts(message, viewables, attachments); + FetchProfile fp = new FetchProfile(); + Message[] localMessages = new Message[] { + message + }; + for (Part part : attachments) { + if (isInlineImage(part) && part.getBody() == null) { + if (remoteFolder == null) { + Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, + localStore.getPersistentCallbacks()); + remoteFolder = remoteStore.getFolder(message.getFolder().getName()); + remoteFolder + .open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); + } + fp.clear(); + fp.add(part); + remoteFolder.fetch(localMessages, fp, null); + localFolder.updateMessage((LocalMessage)message); + synchronized (mListeners) { + for (MessagingListener l : mListeners) { + l.loadInlineImagesForViewOneAvailable(account, message, part); + } + } + } + } + if (remoteFolder != null) { + remoteFolder.close(false); + } + synchronized (mListeners) { + for (MessagingListener l : mListeners) { + l.loadInlineImagesForViewFinished(account, message); + } + } + } catch (Exception e) { + synchronized (mListeners) { + for (MessagingListener l : mListeners) { + l.loadInlineImagesForViewFailed(account, message); + } + } + } + } + + public void loadMessageForView(final Account account, final String folder, final String uid, MessagingListener listener) { synchronized (mListeners) { for (MessagingListener l : mListeners) { @@ -1454,17 +1519,19 @@ public class MessagingController implements Runnable { LocalStore localStore = (LocalStore) Store.getInstance( account.getLocalStoreUri(), mContext, null); /* - * We clear out any attachments already cached in the entire store and then - * we update the passed in message to reflect that there are no cached - * attachments. This is in support of limiting the account to having one - * attachment downloaded at a time. + * We clear out any attachments already cached in the entire store except + * inline images and then we update the passed in message to reflect there are + * no cached attachments. This is in support of limiting the account to having' + * one attachment downloaded at a time. */ localStore.pruneCachedAttachments(); ArrayList viewables = new ArrayList(); ArrayList attachments = new ArrayList(); MimeUtility.collectParts(message, viewables, attachments); for (Part attachment : attachments) { - attachment.setBody(null); + if (!isInlineImage(attachment)) { + attachment.setBody(null); + } } Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, localStore.getPersistentCallbacks()); diff --git a/src/com/android/email/MessagingListener.java b/src/com/android/email/MessagingListener.java index b8271681c..37acede97 100644 --- a/src/com/android/email/MessagingListener.java +++ b/src/com/android/email/MessagingListener.java @@ -87,6 +87,18 @@ public class MessagingListener { public void loadMessageForViewFailed(Account account, String folder, String uid, String message) { } + + public void loadInlineImagesForViewStarted(Account account, Message message) { + } + + public void loadInlineImagesForViewOneAvailable(Account account, Message message, Part part) { + } + + public void loadInlineImagesForViewFinished(Account account, Message message) { + } + + public void loadInlineImagesForViewFailed(Account account, Message message) { + } public void checkMailStarted(Context context, Account account) { } diff --git a/src/com/android/email/activity/MessageView.java b/src/com/android/email/activity/MessageView.java index 957eab3df..2c4ff8714 100644 --- a/src/com/android/email/activity/MessageView.java +++ b/src/com/android/email/activity/MessageView.java @@ -55,7 +55,6 @@ import android.provider.Contacts; import android.provider.Contacts.Intents; import android.provider.Contacts.People; import android.provider.Contacts.Presence; -import android.text.util.Regex; import android.util.Config; import android.util.Log; import android.view.LayoutInflater; @@ -78,7 +77,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Date; -import java.util.regex.Matcher; import java.util.regex.Pattern; public class MessageView extends Activity @@ -97,8 +95,6 @@ public class MessageView extends Activity // Regex that matches start of img tag. '<(?i)img\s+'. private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+"); - // Regex that matches Web URL protocol part as case insensitive. - private static final Pattern WEB_URL_PROTOCOL = Pattern.compile("(?i)http|https://"); private TextView mSubjectView; private TextView mFromView; @@ -141,6 +137,7 @@ public class MessageView extends Activity private static final int MSG_FETCHING_ATTACHMENT = 10; private static final int MSG_SET_SENDER_PRESENCE = 11; private static final int MSG_VIEW_ATTACHMENT_ERROR = 12; + private static final int MSG_FETCHING_PICTURES = 17; @Override public void handleMessage(android.os.Message msg) { @@ -208,6 +205,11 @@ public class MessageView extends Activity getString(R.string.message_view_display_attachment_toast), Toast.LENGTH_SHORT).show(); break; + case MSG_FETCHING_PICTURES: + Toast.makeText(MessageView.this, + getString(R.string.message_view_fetching_pictures_toast), + Toast.LENGTH_SHORT).show(); + break; default: super.handleMessage(msg); } @@ -285,6 +287,10 @@ public class MessageView extends Activity public void attachmentViewError() { sendEmptyMessage(MSG_VIEW_ATTACHMENT_ERROR); } + + public void fetchingPictures() { + sendEmptyMessage(MSG_FETCHING_PICTURES); + } } /** @@ -610,6 +616,16 @@ public class MessageView extends Activity if (mMessage != null) { mMessageContentView.getSettings().setBlockNetworkImage(false); mShowPicturesSection.setVisibility(View.GONE); + new Thread() { + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + MessagingController.getInstance(getApplication()).loadInlineImagesForView( + mAccount, + mMessage, + mListener); + } + }.start(); } } @@ -908,81 +924,26 @@ public class MessageView extends Activity public void loadMessageForViewBodyAvailable(Account account, String folder, String uid, Message message) { MessageView.this.mMessage = message; - try { - Part part = MimeUtility.findFirstPartByMimeType(mMessage, "text/html"); - if (part == null) { - part = MimeUtility.findFirstPartByMimeType(mMessage, "text/plain"); + String text = EmailHtmlUtil.renderMessageText(MessageView.this, account, message); + if (text != null) { + /* + * TODO consider how to get background images and a million other things + * that HTML allows. + */ + // Check if text contains img tag. + if (IMG_TAG_START_REGEX.matcher(text).find()) { + mHandler.showShowPictures(true); } - if (part != null) { - String text = MimeUtility.getTextFromPart(part); - if (part.getMimeType().equalsIgnoreCase("text/html")) { - text = EmailHtmlUtil.resolveInlineImage( - getContentResolver(), mAccount, text, mMessage, 0); - } else { - // And also escape special character, such as "<>&", - // to HTML escape sequence. - text = EmailHtmlUtil.escapeCharacterToDisplay(text); - /* - * Linkify the plain text and convert it to HTML by replacing - * \r?\n with
and adding a html/body wrapper. - */ - StringBuffer sb = new StringBuffer(""); - if (text != null) { - Matcher m = Regex.WEB_URL_PATTERN.matcher(text); - while (m.find()) { - int start = m.start(); - /* - * WEB_URL_PATTERN may match domain part of email address. To detect - * this false match, the character just before the matched string - * should not be '@'. - */ - if (start == 0 || text.charAt(start - 1) != '@') { - String url = m.group(); - Matcher proto = WEB_URL_PROTOCOL.matcher(url); - String link; - if (proto.find()) { - // This is work around to force URL protocol part be lower case, - // because WebView could follow only lower case protocol link. - link = proto.group().toLowerCase() + url.substring(proto.end()); - } else { - // Regex.WEB_URL_PATTERN matches URL without protocol part, - // so added default protocol to link. - link = "http://" + url; - } - String href = String.format("%s", link, url); - m.appendReplacement(sb, href); - } - else { - m.appendReplacement(sb, "$0"); - } - } - m.appendTail(sb); - } - sb.append(""); - text = sb.toString(); - } - - /* - * TODO consider how to get background images and a million other things - * that HTML allows. - */ - // Check if text contains img tag. - if (IMG_TAG_START_REGEX.matcher(text).find()) { - mHandler.showShowPictures(true); - } - - loadMessageContentText(text); - } - else { - loadMessageContentUrl("file:///android_asset/empty.html"); - } - renderAttachments(mMessage, 0); + loadMessageContentText(text); } - catch (Exception e) { - if (Config.LOGV) { - Log.v(Email.LOG_TAG, "loadMessageForViewBodyAvailable", e); - } + else { + loadMessageContentUrl("file:///android_asset/empty.html"); + } + try { + renderAttachments(mMessage, 0); + } catch (MessagingException me) { + // ignore } } @@ -1018,6 +979,35 @@ public class MessageView extends Activity }); } + @Override + public void loadInlineImagesForViewStarted(Account account, Message message) { + mHandler.fetchingPictures(); + } + + @Override + public void loadInlineImagesForViewOneAvailable(final Account account, + final Message message, Part part) { + mHandler.post(new Runnable() { + public void run() { + String text = EmailHtmlUtil.renderMessageText( + MessageView.this, account, message); + if (text != null) { + loadMessageContentText(text); + } + } + }); + } + + @Override + public void loadInlineImagesForViewFinished(Account account, Message message) { + mHandler.progress(false, null); + } + + @Override + public void loadInlineImagesForViewFailed(Account account, Message message) { + mHandler.progress(false, null); + } + @Override public void loadAttachmentStarted(Account account, Message message, Part part, Object tag, boolean requiresDownload) { diff --git a/src/com/android/email/mail/internet/EmailHtmlUtil.java b/src/com/android/email/mail/internet/EmailHtmlUtil.java index bf21831ea..d0fbb4c67 100755 --- a/src/com/android/email/mail/internet/EmailHtmlUtil.java +++ b/src/com/android/email/mail/internet/EmailHtmlUtil.java @@ -17,6 +17,7 @@ package com.android.email.mail.internet; import com.android.email.Account; +import com.android.email.mail.Message; import com.android.email.mail.MessagingException; import com.android.email.mail.Multipart; import com.android.email.mail.Part; @@ -24,16 +25,19 @@ import com.android.email.mail.store.LocalStore.LocalAttachmentBodyPart; import com.android.email.provider.AttachmentProvider; import android.content.ContentResolver; +import android.content.Context; import android.net.Uri; +import android.text.util.Regex; import java.util.regex.Matcher; import java.util.regex.Pattern; public class EmailHtmlUtil { - // Regex that matches characters that have special meaning in HTML. '<', '>', '&' and // multiple continuous spaces. private static final Pattern PLAIN_TEXT_TO_ESCAPE = Pattern.compile("[<>&]| {2,}|\r?\n"); + // Regex that matches Web URL protocol part as case insensitive. + private static final Pattern WEB_URL_PROTOCOL = Pattern.compile("(?i)http|https://"); /** * Resolve content-id reference in src attribute of img tag to AttachmentProvider's @@ -60,7 +64,8 @@ public class EmailHtmlUtil { part instanceof LocalAttachmentBodyPart) { LocalAttachmentBodyPart attachment = (LocalAttachmentBodyPart)part; Uri contentUri = AttachmentProvider.resolveAttachmentIdToContentUri( - resolver, AttachmentProvider.getAttachmentUri(account, attachment.getAttachmentId())); + resolver, + AttachmentProvider.getAttachmentUri(account, attachment.getAttachmentId())); // Regexp which matches ' src="cid:contentId"'. String contentIdRe = "\\s+(?i)src=\"cid(?-i):\\Q" + contentId + "\\E\""; // Replace all occurrences of src attribute with ' src="content://contentUri"'. @@ -116,4 +121,83 @@ public class EmailHtmlUtil { } return text; } + + /** + * Rendering message into HTML text. + */ + public static String renderMessageText(Context context, Account account, + Message message) { + String text = null; + boolean isHtml = false; + try { + Part part = MimeUtility.findFirstPartByMimeType(message, "text/html"); + if (part == null) { + part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); + } + if (part != null) { + text = MimeUtility.getTextFromPart(part); + isHtml = part.getMimeType().equalsIgnoreCase("text/html"); + } + } catch (MessagingException me) { + // ignore + } + if (text == null) { + return null; + } + + if (isHtml) { + try { + text = resolveInlineImage(context.getContentResolver(), account, text, message, 0); + } catch (MessagingException me) { + // ignore + } + + } else { + // And also escape special character, such as "<>&\n", + // to HTML escape sequence. + text = escapeCharacterToDisplay(text); + + /* + * Linkify the plain text and convert it to HTML by replacing \r?\n + * with
and adding a html/body wrapper. + */ + StringBuffer sb = new StringBuffer(""); + if (text != null) { + Matcher m = Regex.WEB_URL_PATTERN.matcher(text); + while (m.find()) { + /* + * WEB_URL_PATTERN may match domain part of email address. To + * detect this false match, the character just before the + * matched string should not be '@'. + */ + int start = m.start(); + if (start == 0 || text.charAt(start - 1) != '@') { + String url = m.group(); + Matcher proto = WEB_URL_PROTOCOL.matcher(url); + String link; + if (proto.find()) { + // This is work around to force URL protocol part be + // lower case, + // because WebView could follow only lower case protocol + // link. + link = proto.group().toLowerCase() + url.substring(proto.end()); + } else { + // Regex.WEB_URL_PATTERN matches URL without protocol + // part, + // so added default protocol to link. + link = "http://" + url; + } + String href = String.format("%s", link, url); + m.appendReplacement(sb, href); + } else { + m.appendReplacement(sb, "$0"); + } + } + m.appendTail(sb); + } + sb.append(""); + text = sb.toString(); + } + return text; + } } diff --git a/src/com/android/email/mail/store/LocalStore.java b/src/com/android/email/mail/store/LocalStore.java index be2651835..75c7cc1ca 100644 --- a/src/com/android/email/mail/store/LocalStore.java +++ b/src/com/android/email/mail/store/LocalStore.java @@ -379,17 +379,23 @@ public class LocalStore extends Store implements PersistentDataCallbacks { try { cursor = mDb.query( "attachments", - new String[] { "store_data" }, + new String[] { "store_data", "mime_type", "content_id" }, "id = ?", new String[] { file.getName() }, null, null, null); if (cursor.moveToNext()) { - if (cursor.getString(0) == null) { + String storeData = cursor.getString(0); + String mimeType = cursor.getString(1); + String contentId = cursor.getString(2); + boolean inlineImage = contentId != null + && (mimeType != null && mimeType.startsWith("image/")); + if (storeData == null || inlineImage) { /* * If the attachment has no store data it is not recoverable, so - * we won't delete it. + * we won't delete it. And if the attachment is image and has + * content id, so we won't delete it because it is inline image. */ continue; } @@ -1277,8 +1283,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks { message.mId }); - for (int i = 0, count = attachments.size(); i < count; i++) { - Part attachment = attachments.get(i); + for (Part attachment : attachments) { saveAttachment(message.mId, attachment, false); } } catch (Exception e) { @@ -1302,6 +1307,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks { if ((!saveAsNew) && (attachment instanceof LocalAttachmentBodyPart)) { attachmentId = ((LocalAttachmentBodyPart) attachment).getAttachmentId(); + size = ((LocalAttachmentBodyPart) attachment).getSize(); } if (attachment.getBody() != null) { @@ -1345,6 +1351,9 @@ public class LocalStore extends Store implements PersistentDataCallbacks { MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA), ','); String name = MimeUtility.getHeaderParameter(attachment.getContentType(), "name"); + if (name == null) { + name = MimeUtility.getHeaderParameter(attachment.getDisposition(), "filename"); + } String contentId = attachment.getContentId(); if (attachmentId == -1) { diff --git a/tests/src/com/android/email/mail/internet/EmailHtmlUtilTest.java b/tests/src/com/android/email/mail/internet/EmailHtmlUtilTest.java index aae36ff92..1e441be89 100755 --- a/tests/src/com/android/email/mail/internet/EmailHtmlUtilTest.java +++ b/tests/src/com/android/email/mail/internet/EmailHtmlUtilTest.java @@ -53,6 +53,8 @@ public class EmailHtmlUtilTest extends AndroidTestCase { // This is needed for mime image bodypart. BinaryTempFileBody.setTempDirectory(getContext().getCacheDir()); } + + // TODO write test for renderMessageText() /** * Tests for resolving inline image src cid: reference to content uri. From 14f3e1679369977420e0f0905f23477110af64dc Mon Sep 17 00:00:00 2001 From: The Android Open Source Project Date: Mon, 15 Jun 2009 00:01:17 -0700 Subject: [PATCH 2/2] IA 149719: Display Bcc: field if present... --- res/layout/message_view_header.xml | 23 +++++++++++++++++++ res/values/strings.xml | 2 ++ .../android/email/activity/MessageView.java | 12 +++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/res/layout/message_view_header.xml b/res/layout/message_view_header.xml index 18fe4eabd..4a526b9bf 100644 --- a/res/layout/message_view_header.xml +++ b/res/layout/message_view_header.xml @@ -112,6 +112,29 @@ android:singleLine="false" android:ellipsize="none" /> + + + + To: Cc: + + Bcc: Open diff --git a/src/com/android/email/activity/MessageView.java b/src/com/android/email/activity/MessageView.java index 2c4ff8714..7ace7f944 100644 --- a/src/com/android/email/activity/MessageView.java +++ b/src/com/android/email/activity/MessageView.java @@ -102,7 +102,9 @@ public class MessageView extends Activity private TextView mTimeView; private TextView mToView; private TextView mCcView; + private TextView mBccView; private View mCcContainerView; + private View mBccContainerView; private WebView mMessageContentView; private LinearLayout mAttachments; private ImageView mAttachmentIcon; @@ -173,6 +175,9 @@ public class MessageView extends Activity mToView.setText(values[4]); mCcView.setText(values[5]); mCcContainerView.setVisibility((values[5] != null) ? View.VISIBLE : View.GONE); + String bcc = values[6]; + mBccView.setText(bcc); + mBccContainerView.setVisibility(bcc != null ? View.VISIBLE : View.GONE); mAttachmentIcon.setVisibility(msg.arg1 == 1 ? View.VISIBLE : View.GONE); break; case MSG_NETWORK_ERROR: @@ -244,11 +249,12 @@ public class MessageView extends Activity String date, String to, String cc, + String bcc, boolean hasAttachments) { android.os.Message msg = new android.os.Message(); msg.what = MSG_SET_HEADERS; msg.arg1 = hasAttachments ? 1 : 0; - msg.obj = new String[] { subject, from, time, date, to, cc }; + msg.obj = new String[] { subject, from, time, date, to, cc, bcc}; sendMessage(msg); } @@ -337,6 +343,8 @@ public class MessageView extends Activity mToView = (TextView) findViewById(R.id.to); mCcView = (TextView) findViewById(R.id.cc); mCcContainerView = findViewById(R.id.cc_container); + mBccView = (TextView) findViewById(R.id.bcc); + mBccContainerView = findViewById(R.id.bcc_container); mDateView = (TextView) findViewById(R.id.date); mTimeView = (TextView) findViewById(R.id.time); mMessageContentView = (WebView) findViewById(R.id.message_content); @@ -903,6 +911,7 @@ public class MessageView extends Activity mDateFormat.format(sentDate); String toText = Address.toFriendly(message.getRecipients(RecipientType.TO)); String ccText = Address.toFriendly(message.getRecipients(RecipientType.CC)); + String bccText = Address.toFriendly(message.getRecipients(RecipientType.BCC)); boolean hasAttachments = ((LocalMessage) message).getAttachmentCount() > 0; mHandler.setHeaders(subjectText, fromText, @@ -910,6 +919,7 @@ public class MessageView extends Activity dateText, toText, ccText, + bccText, hasAttachments); startPresenceCheck(); }