AI 149523: Correctly display inline images in Reply and Forward messages.

Integrates CL 148436, 148515, 148833 from imode email.
  BUG=1814789,1860250

Automated import of CL 149523
This commit is contained in:
Mihai Preda 2009-06-03 06:44:47 -07:00 committed by The Android Open Source Project
parent 39137e51aa
commit 7436601fae
6 changed files with 293 additions and 180 deletions

View File

@ -32,6 +32,7 @@ import com.android.email.mail.MessagingException;
import com.android.email.mail.Multipart;
import com.android.email.mail.Part;
import com.android.email.mail.Message.RecipientType;
import com.android.email.mail.internet.EmailHtmlUtil;
import com.android.email.mail.internet.MimeBodyPart;
import com.android.email.mail.internet.MimeHeader;
import com.android.email.mail.internet.MimeMessage;
@ -1133,16 +1134,25 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus
}
}
Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain");
Boolean plainTextFlag = false;
Part part = MimeUtility.findFirstPartByMimeType(message, "text/html");
if (part == null) {
part = MimeUtility.findFirstPartByMimeType(message, "text/html");
part = MimeUtility.findFirstPartByMimeType(message, "text/plain");
plainTextFlag = true;
}
if (part != null) {
String text = MimeUtility.getTextFromPart(part);
if (text != null) {
if (!plainTextFlag) {
text = EmailHtmlUtil.resolveInlineImage(
getContentResolver(), mAccount, text, message, 0);
}
text = EmailHtmlUtil.escapeCharacterToDisplay(
text, plainTextFlag);
mQuotedTextBar.setVisibility(View.VISIBLE);
mQuotedText.setVisibility(View.VISIBLE);
mQuotedText.loadDataWithBaseURL("email://", text, part.getMimeType(),
mQuotedText.loadDataWithBaseURL("email://", text, "text/html",
"utf-8", null);
}
}
@ -1164,16 +1174,26 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus
mSubjectView.setText(message.getSubject());
}
Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain");
Boolean plainTextFlag = false;
Part part = MimeUtility.findFirstPartByMimeType(message, "text/html");
if (part == null) {
part = MimeUtility.findFirstPartByMimeType(message, "text/html");
part = MimeUtility.findFirstPartByMimeType(message, "text/plain");
plainTextFlag = true;
}
if (part != null) {
String text = MimeUtility.getTextFromPart(part);
if (text != null) {
if (!plainTextFlag) {
text = EmailHtmlUtil.resolveInlineImage(
getContentResolver(), mAccount, text, message, 0);
}
text = EmailHtmlUtil.escapeCharacterToDisplay(
text, plainTextFlag);
mQuotedTextBar.setVisibility(View.VISIBLE);
mQuotedText.setVisibility(View.VISIBLE);
mQuotedText.loadDataWithBaseURL("email://", text, part.getMimeType(),
mQuotedText.loadDataWithBaseURL("email://", text, "text/html",
"utf-8", null);
}
}

View File

@ -28,6 +28,7 @@ import com.android.email.mail.MessagingException;
import com.android.email.mail.Multipart;
import com.android.email.mail.Part;
import com.android.email.mail.Message.RecipientType;
import com.android.email.mail.internet.EmailHtmlUtil;
import com.android.email.mail.internet.MimeUtility;
import com.android.email.mail.store.LocalStore.LocalAttachmentBodyPart;
import com.android.email.mail.store.LocalStore.LocalMessage;
@ -714,69 +715,6 @@ public class MessageView extends Activity
}
}
/**
* Resolve attachment id to content URI.
*
* @param attachmentUri
* @return resolved content URI
*/
private Uri resolveAttachmentIdToContentUri(long attachmentId) {
Uri attachmentUri = AttachmentProvider.getAttachmentUri(mAccount, attachmentId);
Cursor c = getContentResolver().query(attachmentUri,
new String[] { AttachmentProvider.AttachmentProviderColumns.DATA },
null, null, null);
if (c != null) {
try {
if (c.moveToFirst()) {
return Uri.parse(c.getString(0));
}
} finally {
c.close();
}
}
return attachmentUri;
}
/**
* Resolve content-id reference in src attribute of img tag to AttachmentProvider's
* content uri. This method calls itself recursively at most the number of
* LocalAttachmentPart that mime type is image and has content id.
* The attribute src="cid:content_id" is resolved as src="content://...".
* This method is package scope for testing purpose.
*
* @param text html email text
* @param part mime part which may contain inline image
* @return html text in which src attribute of img tag may be replaced with content uri
*/
/* package */ String resolveInlineImage(String text, Part part, int depth)
throws MessagingException {
// avoid too deep recursive call or null text
if (depth >= 10 || text == null) {
return text;
}
String contentType = MimeUtility.unfoldAndDecode(part.getContentType());
String contentId = part.getContentId();
if (contentType.startsWith("image/") &&
contentId != null &&
part instanceof LocalAttachmentBodyPart) {
LocalAttachmentBodyPart attachment = (LocalAttachmentBodyPart)part;
Uri contentUri = resolveAttachmentIdToContentUri(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"'.
text = text.replaceAll(contentIdRe, " src=\"" + contentUri + "\"");
}
if (part.getBody() instanceof Multipart) {
Multipart mp = (Multipart)part.getBody();
for (int i = 0; i < mp.getCount(); i++) {
text = resolveInlineImage(text, mp.getBodyPart(i), depth + 1);
}
}
return text;
}
private void renderAttachments(Part part, int depth) throws MessagingException {
String contentType = MimeUtility.unfoldAndDecode(part.getContentType());
String name = MimeUtility.getHeaderParameter(contentType, "name");
@ -958,7 +896,8 @@ public class MessageView extends Activity
if (part != null) {
String text = MimeUtility.getTextFromPart(part);
if (part.getMimeType().equalsIgnoreCase("text/html")) {
text = resolveInlineImage(text, mMessage, 0);
text = EmailHtmlUtil.resolveInlineImage(
getContentResolver(), mAccount, text, mMessage, 0);
} else {
/*
* Linkify the plain text and convert it to HTML by replacing
@ -1066,7 +1005,9 @@ public class MessageView extends Activity
try {
File file = createUniqueFile(Environment.getExternalStorageDirectory(),
attachment.name);
Uri uri = resolveAttachmentIdToContentUri(attachment.part.getAttachmentId());
Uri uri = AttachmentProvider.resolveAttachmentIdToContentUri(
getContentResolver(), AttachmentProvider.getAttachmentUri(
mAccount, attachment.part.getAttachmentId()));
InputStream in = getContentResolver().openInputStream(uri);
OutputStream out = new FileOutputStream(file);
IOUtils.copy(in, out);
@ -1082,7 +1023,9 @@ public class MessageView extends Activity
}
else {
try {
Uri uri = resolveAttachmentIdToContentUri(attachment.part.getAttachmentId());
Uri uri = AttachmentProvider.resolveAttachmentIdToContentUri(
getContentResolver(), AttachmentProvider.getAttachmentUri(
mAccount, attachment.part.getAttachmentId()));
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
@ -1173,3 +1116,4 @@ public class MessageView extends Activity
}
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.mail.internet;
import com.android.email.Account;
import com.android.email.mail.MessagingException;
import com.android.email.mail.Multipart;
import com.android.email.mail.Part;
import com.android.email.mail.store.LocalStore.LocalAttachmentBodyPart;
import com.android.email.provider.AttachmentProvider;
import android.content.ContentResolver;
import android.net.Uri;
public class EmailHtmlUtil {
/**
* Resolve content-id reference in src attribute of img tag to AttachmentProvider's
* content uri. This method calls itself recursively at most the number of
* LocalAttachmentPart that mime type is image and has content id.
* The attribute src="cid:content_id" is resolved as src="content://...".
* This method is package scope for testing purpose.
*
* @param text html email text
* @param part mime part which may contain inline image
* @return html text in which src attribute of img tag may be replaced with content uri
*/
public static String resolveInlineImage(
ContentResolver resolver, Account account, String text, Part part, int depth)
throws MessagingException {
// avoid too deep recursive call.
if (depth >= 10 || text == null) {
return text;
}
String contentType = MimeUtility.unfoldAndDecode(part.getContentType());
String contentId = part.getContentId();
if (contentType.startsWith("image/") &&
contentId != null &&
part instanceof LocalAttachmentBodyPart) {
LocalAttachmentBodyPart attachment = (LocalAttachmentBodyPart)part;
Uri contentUri = AttachmentProvider.resolveAttachmentIdToContentUri(
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"'.
text = text.replaceAll(contentIdRe, " src=\"" + contentUri + "\"");
}
if (part.getBody() instanceof Multipart) {
Multipart mp = (Multipart)part.getBody();
for (int i = 0; i < mp.getCount(); i++) {
text = resolveInlineImage(resolver, account, text, mp.getBodyPart(i), depth + 1);
}
}
return text;
}
public static String escapeCharacterToDisplay(String text, boolean plainText) {
// TODO: implement html escaping as in CL 145919, 148437 to fix bug 1785319
return text;
}
}

View File

@ -25,6 +25,7 @@ import java.io.InputStream;
import java.util.List;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
@ -298,4 +299,25 @@ public class AttachmentProvider extends ContentProvider {
return null;
}
}
/**
* Resolve attachment id to content URI.
*
* @param attachmentUri
* @return resolved content URI
*/
public static Uri resolveAttachmentIdToContentUri(ContentResolver resolver, Uri attachmentUri) {
Cursor c = resolver.query(attachmentUri,
new String[] { AttachmentProvider.AttachmentProviderColumns.DATA },
null, null, null);
if (c != null) {
try {
if (c.moveToFirst()) {
return Uri.parse(c.getString(0));
}
} finally {
c.close();
}
}
return attachmentUri;
}
}

View File

@ -28,8 +28,10 @@ import com.android.email.mail.MessageTestUtils.MessageBuilder;
import com.android.email.mail.MessageTestUtils.MultipartBuilder;
import com.android.email.mail.MessageTestUtils.TextBuilder;
import com.android.email.mail.internet.BinaryTempFileBody;
import com.android.email.mail.internet.EmailHtmlUtil;
import com.android.email.mail.store.LocalStore;
import android.app.Application;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
@ -38,6 +40,7 @@ import android.net.Uri;
import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.Suppress;
import android.util.Log;
import android.webkit.WebView;
import android.widget.TextView;
@ -78,6 +81,7 @@ public class MessageViewTests
@Override
protected void setUp() throws Exception {
super.setUp();
mContext = getInstrumentation().getTargetContext();
Account[] accounts = Preferences.getPreferences(mContext).getAccounts();
if (accounts.length > 0)
@ -86,14 +90,11 @@ public class MessageViewTests
mAccount = Preferences.getPreferences(mContext).getDefaultAccount();
Email.setServicesEnabled(mContext);
}
// configure a mock controller
MessagingController mockController = new MockMessagingController();
MessagingController.injectMockController(mockController);
// setup an intent to spin up this activity with something useful
ArrayList<String> FOLDER_UIDS = new ArrayList<String>(
Arrays.asList(new String[]{ "why", "is", "java", "so", "ugly?" }));
// Log.d("MessageViewTest", "--- folder:" + FOLDER_UIDS);
Intent i = new Intent()
.putExtra(EXTRA_ACCOUNT, mAccount)
.putExtra(EXTRA_FOLDER, FOLDER_NAME)
@ -101,6 +102,11 @@ public class MessageViewTests
.putStringArrayListExtra(EXTRA_FOLDER_UIDS, FOLDER_UIDS);
this.setActivityIntent(i);
// configure a mock controller
MessagingController mockController =
new MockMessagingController(getActivity().getApplication());
MessagingController.injectMockController(mockController);
final MessageView a = getActivity();
mToView = (TextView) a.findViewById(R.id.to);
mSubjectView = (TextView) a.findViewById(R.id.subject);
@ -151,115 +157,14 @@ public class MessageViewTests
a.handleMenuItem(R.id.mark_as_unread);
}
/**
* Tests for resolving inline image src cid: reference to content uri.
*/
public void testResolveInlineImage() throws MessagingException, IOException {
final MessageView a = getActivity();
final LocalStore store = (LocalStore) LocalStore.newInstance(mAccount.getLocalStoreUri(),
mContext, null);
// Single cid case.
final String cid1 = "cid.1@android.com";
final long aid1 = 10;
final Uri uri1 = MessageTestUtils.contentUri(aid1, mAccount);
final String text1 = new TextBuilder("text1 > ").addCidImg(cid1).build(" <.");
final String expected1 = new TextBuilder("text1 > ").addUidImg(uri1).build(" <.");
// message with cid1
final Message msg1 = new MessageBuilder()
.setBody(new MultipartBuilder("multipart/related")
.addBodyPart(MessageTestUtils.textPart("text/html", text1))
.addBodyPart(MessageTestUtils.imagePart("image/jpeg", "<"+cid1+">", aid1, store))
.build())
.build();
// Simple case.
final String actual1 = a.resolveInlineImage(text1, msg1, 0);
assertEquals("one content id reference is not resolved",
expected1, actual1);
// Exceed recursive limit.
final String actual0 = a.resolveInlineImage(text1, msg1, 10);
assertEquals("recursive call limit may exceeded",
text1, actual0);
// Multiple cids case.
final String cid2 = "cid.2@android.com";
final long aid2 = 20;
final Uri uri2 = MessageTestUtils.contentUri(aid2, mAccount);
final String text2 = new TextBuilder("text2 ").addCidImg(cid2).build(".");
final String expected2 = new TextBuilder("text2 ").addUidImg(uri2).build(".");
// message with only cid2
final Message msg2 = new MessageBuilder()
.setBody(new MultipartBuilder("multipart/related")
.addBodyPart(MessageTestUtils.textPart("text/html", text1 + text2))
.addBodyPart(MessageTestUtils.imagePart("image/gif", cid2, aid2, store))
.build())
.build();
// cid1 is not replaced
final String actual2 = a.resolveInlineImage(text1 + text2, msg2, 0);
assertEquals("only one of two content id is resolved",
text1 + expected2, actual2);
// message with cid1 and cid2
final Message msg3 = new MessageBuilder()
.setBody(new MultipartBuilder("multipart/related")
.addBodyPart(MessageTestUtils.textPart("text/html", text2 + text1))
.addBodyPart(MessageTestUtils.imagePart("image/jpeg", cid1, aid1, store))
.addBodyPart(MessageTestUtils.imagePart("image/gif", cid2, aid2, store))
.build())
.build();
// cid1 and cid2 are replaced
final String actual3 = a.resolveInlineImage(text2 + text1, msg3, 0);
assertEquals("two content ids are resolved correctly",
expected2 + expected1, actual3);
// message with many cids and normal attachments
final Message msg4 = new MessageBuilder()
.setBody(new MultipartBuilder("multipart/mixed")
.addBodyPart(MessageTestUtils.imagePart("image/jpeg", null, 30, store))
.addBodyPart(MessageTestUtils.imagePart("application/pdf", cid1, aid1, store))
.addBodyPart(new MultipartBuilder("multipart/related")
.addBodyPart(MessageTestUtils.textPart("text/html", text2 + text1))
.addBodyPart(MessageTestUtils.imagePart("image/jpg", cid1, aid1, store))
.addBodyPart(MessageTestUtils.imagePart("image/gif", cid2, aid2, store))
.buildBodyPart())
.addBodyPart(MessageTestUtils.imagePart("application/pdf", cid2, aid2, store))
.build())
.build();
// cid1 and cid2 are replaced
final String actual4 = a.resolveInlineImage(text2 + text1, msg4, 0);
assertEquals("two content ids in deep multipart level are resolved",
expected2 + expected1, actual4);
// No crash on null text
final String actual5 = a.resolveInlineImage(null, msg4, 0);
assertNull(actual5);
}
/**
* Test for resolveAttachmentIdToContentUri.
*/
public void testResolveAttachmentIdToContentUri() throws MessagingException, IOException {
final ContentResolver contentResolver = mContext.getContentResolver();
final MessageView a = getActivity();
// create attachments tables.
LocalStore.newInstance(mAccount.getLocalStoreUri(), mContext, null);
final String dbPath = mContext.getDatabasePath(mAccount.getUuid() + ".db").toString();
final SQLiteDatabase db = SQLiteDatabase.openDatabase(dbPath, null, 0);
// TODO write unit test
}
/**
* Mock Messaging controller, so we can drive its callbacks. This probably should be
* generalized since we're likely to use for other tests eventually.
*/
private static class MockMessagingController extends MessagingController {
private MockMessagingController() {
super(null);
private MockMessagingController(Application application) {
super(application);
}
}

View File

@ -0,0 +1,144 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.mail.internet;
import com.android.email.Account;
import com.android.email.Preferences;
import com.android.email.mail.Message;
import com.android.email.mail.MessageTestUtils;
import com.android.email.mail.MessagingException;
import com.android.email.mail.MessageTestUtils.MessageBuilder;
import com.android.email.mail.MessageTestUtils.MultipartBuilder;
import com.android.email.mail.MessageTestUtils.TextBuilder;
import com.android.email.mail.store.LocalStore;
import android.net.Uri;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
import java.io.IOException;
@MediumTest
public class EmailHtmlUtilTest extends AndroidTestCase {
private Account mAccount;
@Override
protected void setUp() throws Exception {
super.setUp();
Account[] accounts = Preferences.getPreferences(mContext).getAccounts();
if (accounts.length > 0)
{
// This depends on getDefaultAccount() to auto-assign the default account, if necessary
mAccount = Preferences.getPreferences(mContext).getAccounts()[0];
}
// This is needed for mime image bodypart.
BinaryTempFileBody.setTempDirectory(getContext().getCacheDir());
}
/**
* Tests for resolving inline image src cid: reference to content uri.
*/
public void testResolveInlineImage() throws MessagingException, IOException {
final LocalStore store = (LocalStore) LocalStore.newInstance(mAccount.getLocalStoreUri(),
mContext, null);
// Single cid case.
final String cid1 = "cid.1@android.com";
final long aid1 = 10;
final Uri uri1 = MessageTestUtils.contentUri(aid1, mAccount);
final String text1 = new TextBuilder("text1 > ").addCidImg(cid1).build(" <.");
final String expected1 = new TextBuilder("text1 > ").addUidImg(uri1).build(" <.");
// message with cid1
final Message msg1 = new MessageBuilder()
.setBody(new MultipartBuilder("multipart/related")
.addBodyPart(MessageTestUtils.textPart("text/html", text1))
.addBodyPart(MessageTestUtils.imagePart("image/jpeg", "<"+cid1+">", aid1, store))
.build())
.build();
// Simple case.
final String actual1 = EmailHtmlUtil.resolveInlineImage(
getContext().getContentResolver(), mAccount, text1, msg1, 0);
assertEquals("one content id reference is not resolved",
expected1, actual1);
// Exceed recursive limit.
final String actual0 = EmailHtmlUtil.resolveInlineImage(
getContext().getContentResolver(), mAccount, text1, msg1, 10);
assertEquals("recursive call limit may exceeded",
text1, actual0);
// Multiple cids case.
final String cid2 = "cid.2@android.com";
final long aid2 = 20;
final Uri uri2 = MessageTestUtils.contentUri(aid2, mAccount);
final String text2 = new TextBuilder("text2 ").addCidImg(cid2).build(".");
final String expected2 = new TextBuilder("text2 ").addUidImg(uri2).build(".");
// message with only cid2
final Message msg2 = new MessageBuilder()
.setBody(new MultipartBuilder("multipart/related")
.addBodyPart(MessageTestUtils.textPart("text/html", text1 + text2))
.addBodyPart(MessageTestUtils.imagePart("image/gif", cid2, aid2, store))
.build())
.build();
// cid1 is not replaced
final String actual2 = EmailHtmlUtil.resolveInlineImage(
getContext().getContentResolver(), mAccount, text1 + text2, msg2, 0);
assertEquals("only one of two content id is resolved",
text1 + expected2, actual2);
// message with cid1 and cid2
final Message msg3 = new MessageBuilder()
.setBody(new MultipartBuilder("multipart/related")
.addBodyPart(MessageTestUtils.textPart("text/html", text2 + text1))
.addBodyPart(MessageTestUtils.imagePart("image/jpeg", cid1, aid1, store))
.addBodyPart(MessageTestUtils.imagePart("image/gif", cid2, aid2, store))
.build())
.build();
// cid1 and cid2 are replaced
final String actual3 = EmailHtmlUtil.resolveInlineImage(
getContext().getContentResolver(), mAccount, text2 + text1, msg3, 0);
assertEquals("two content ids are resolved correctly",
expected2 + expected1, actual3);
// message with many cids and normal attachments
final Message msg4 = new MessageBuilder()
.setBody(new MultipartBuilder("multipart/mixed")
.addBodyPart(MessageTestUtils.imagePart("image/jpeg", null, 30, store))
.addBodyPart(MessageTestUtils.imagePart("application/pdf", cid1, aid1, store))
.addBodyPart(new MultipartBuilder("multipart/related")
.addBodyPart(MessageTestUtils.textPart("text/html", text2 + text1))
.addBodyPart(MessageTestUtils.imagePart("image/jpg", cid1, aid1, store))
.addBodyPart(MessageTestUtils.imagePart("image/gif", cid2, aid2, store))
.buildBodyPart())
.addBodyPart(MessageTestUtils.imagePart("application/pdf", cid2, aid2, store))
.build())
.build();
// cid1 and cid2 are replaced
final String actual4 = EmailHtmlUtil.resolveInlineImage(
getContext().getContentResolver(), mAccount, text2 + text1, msg4, 0);
assertEquals("two content ids in deep multipart level are resolved",
expected2 + expected1, actual4);
// No crash on null text
final String actual5 = EmailHtmlUtil.resolveInlineImage(getContext().getContentResolver(),
mAccount, null, msg4, 0);
assertNull(actual5);
}
}